fix: honor request cache-control max-age=0 in cache interceptor#5510
Open
jeswr wants to merge 2 commits into
Open
fix: honor request cache-control max-age=0 in cache interceptor#5510jeswr wants to merge 2 commits into
jeswr wants to merge 2 commits into
Conversation
The request max-age check in lib/interceptor/cache.js used
if (reqCacheControl?.['max-age'] && age >= reqCacheControl['max-age']) {
which is falsy for max-age=0, so the directive was silently ignored and a
cached response was served with no origin contact. RFC 9111 section 5.2.1.1
requires validation in that case. This also broke
fetch(url, { cache: 'no-cache' }) through a composed cache dispatcher, since
the fetch spec implements that mode by appending Cache-Control: max-age=0.
Additionally, when a nonzero request max-age was exceeded, the old code
bypassed the cache with a plain dispatch(opts, handler) (no CacheHandler),
so the fresh response fetched because of the bypass was never stored and the
next plain request had to contact the origin again.
Fix: treat request max-age exceedance as a revalidation trigger in
needsRevalidation(), feeding it into the existing revalidation flow (which
sends the conditional request, serves the cached body on 304, and stores a
fresh 200 via CacheHandler).
Before (mnot cache-tests): ccreq-ma0 "N no"; after: "Y yes".
Fixes nodejs#5504
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 4, 2026
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5510 +/- ##
=======================================
Coverage 93.44% 93.44%
=======================================
Files 110 110
Lines 37328 37331 +3
=======================================
+ Hits 34881 34884 +3
Misses 2447 2447 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This was referenced Jul 4, 2026
mcollina
requested changes
Jul 4, 2026
| return true | ||
| } | ||
|
|
||
| // Always revalidate requests whose age exceeds the max-age request |
Member
There was a problem hiding this comment.
This comment is incredibly long. Shorten it
Contributor
Author
There was a problem hiding this comment.
Shortened in a10a00d — kept the max-age=0/falsy caveat and the RFC 9111 §5.2.1.1 ref. Thanks.
jeswr
pushed a commit
to jeswr/undici
that referenced
this pull request
Jul 4, 2026
Addresses review feedback from mcollina on nodejs#5510: the max-age revalidation comment was too long. Trimmed to one sentence of rationale while keeping the max-age=0/falsy caveat and RFC 9111 ref. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses review feedback from mcollina on nodejs#5510: the max-age revalidation comment was too long. Trimmed to one sentence of rationale while keeping the max-age=0/falsy caveat and RFC 9111 ref. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
a10a00d to
852ce74
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5504
The bug
lib/interceptor/cache.jschecked the requestmax-agedirective withFor
max-age=0the parsed value0is falsy, so the directive was ignored entirely and a cached response was served with no origin contact. RFC 9111 §5.2.1.1 requires validation in that case. This also brokefetch(url, { cache: 'no-cache' })through a composed cache dispatcher, since the fetch spec implements that mode by appendingCache-Control: max-age=0(related umbrella: #3847).Secondary issue in the same block: when a nonzero request
max-agewas exceeded, the bypass dispatched without aCacheHandler(return dispatch(opts, handler)), so the fresh 200 fetched because of the bypass was never stored — the very next plain request had to contact the origin again.The fix
Treat request
max-ageexceedance as a revalidation trigger inneedsRevalidation()(alongside the existing requestno-cachehandling), feeding it into the existing revalidation flow: a conditional request is sent, the cached body is served on 304, and a fresh 200 is stored via theCacheHandleralready wrapped byCacheRevalidationHandler. The old falsy-guarded bypass block is removed.Evidence
Repro from the issue (fresh cached response, then a request with
Cache-Control: max-age=0):1— silent cache hit, no validationIf-None-Match), hits go to2, cached body served on 304Same for
fetch(url, { cache: 'no-cache' })throughnew Agent().compose(interceptors.cache(...)): before, 1 origin hit (revalidation never sent); after, the validation request is sent.mnot cache-tests conformance runner (
node test/cache-interceptor/cache-tests.mjs):ccreq-ma0("Does HTTP cache honor requestCache-Control: max-age=0when it holds a fresh response?"): "N no" → "Y yes"mainbaseline; theccreq-*checks are behavioral yes/no entries that don't count toward pass/fail totals). Nothing in the runner's skip-list relates to requestmax-age, so no un-skips were needed.New tests in
test/interceptors/cache.js(all fail onmain, pass with this change):max-age=0on a fresh cached response triggers a validation request and serves the cached body on 304max-ageis stored, so the next plain request is a cache hit with the new bodyfetch(url, { cache: 'no-cache' })through a composed cache dispatcher sends a validation requestFull cache test files (
test/interceptors/cache*.js,test/cache-interceptor/*): 124 tests / 12 suites, 124 pass, 0 fail (baseline onmainis 121/121; the +3 are the new tests).Notes
must-revalidatevs requestmax-stale(cache: response "must-revalidate" ignored when request has "max-stale" — stale served without validation (RFC 9111 §5.2.2.2) #5508).