fix: enforce must-revalidate and proxy-revalidate over max-stale and stale-if-error#5511
Open
jeswr wants to merge 3 commits into
Open
fix: enforce must-revalidate and proxy-revalidate over max-stale and stale-if-error#5511jeswr wants to merge 3 commits into
jeswr wants to merge 3 commits into
Conversation
…stale-if-error RFC 9111 §5.2.2.2: a cache MUST NOT use a stale response with must-revalidate to satisfy any request without successful validation — this takes precedence over the request's max-stale grace (§5.2.1.2) and excludes the response from stale-if-error (RFC 5861 §4). proxy-revalidate (§5.2.2.8) has the same semantics for shared caches (the interceptor's default type) and was parsed but never enforced. Before: store `200 Cache-Control: max-age=1, must-revalidate` (+ ETag), wait past staleness, request with `Cache-Control: max-stale=600` — the stale response is served straight from cache with zero origin contact. After: a conditional request is sent to the origin and the cached response is only reused on successful validation. isStale() now skips the max-stale grace window and handleResult() skips the stale-if-error threshold when the stored response carries must-revalidate (or proxy-revalidate on a shared cache). Fixes nodejs#5508 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 #5511 +/- ##
=======================================
Coverage 93.44% 93.44%
=======================================
Files 110 110
Lines 37328 37345 +17
=======================================
+ Hits 34881 34898 +17
Misses 2447 2447 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
7 tasks
d49a8eb to
5d8f3ba
Compare
'Should end h2 zero-length request bodies with headers' failed with ERR_OSSL_ASN1_ILLEGAL_PADDING from a runtime-generated self-signed cert (@metcoder95/https-pem). The plain Node 26/ubuntu job passed on the same commit and the test passes locally on this branch and on main; this PR only touches lib/interceptor/cache.js. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses review feedback by tightening the forbidsServingStale JSDoc to match undici's terse comment style; RFC references retained. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5d8f3ba to
c391280
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.
What
The cache interceptor's
isStale()granted the requestmax-stalegrace window without consulting the stored response's directives, andhandleResult()applied thestale-if-errorthreshold unconditionally.RFC 9111 §5.2.2.2: a cache MUST NOT use a stale response with
must-revalidateto satisfy any request without successful validation — this overrides the request'smax-stale(see the note in §5.2.1.2) and also excludes the response fromstale-if-error(RFC 5861 §4).proxy-revalidatehas the same semantics for shared caches — the interceptor's defaulttype— and was parsed but never enforced anywhere.Fixes #5508
Repro
Store
200 Cache-Control: max-age=1, must-revalidate(+ ETag), wait past staleness, then request withCache-Control: max-stale=600:if-none-match/if-modified-since) sent to the origin; the cached response is only reused after successful validation.Same for
stale-if-error: a stalemust-revalidateresponse was previously served when revalidation returned a 5xx within thestale-if-errorwindow; now the error is propagated.Changes
lib/interceptor/cache.js: newforbidsServingStale(result, cacheType)helper;isStale()skips themax-stalegrace window andhandleResult()skips thestale-if-errorthreshold when the stored response hasmust-revalidate(orproxy-revalidateon a shared cache).proxy-revalidatecorrectly remains inert fortype: 'private'caches.test/interceptors/cache.js: three new tests (must-revalidate vs max-stale, proxy-revalidate shared vs private, must-revalidate excluded from stale-if-error), all failing before the fix and passing after.Testing
test/interceptors/cache*.js,test/cache-interceptor/*.js): 124 tests, 124 pass, 0 fail (121 baseline + 3 new).node test/cache-interceptor/cache-tests.mjs): identical to main's baseline — 283 total, 220 passed, 0 failed, 58 failed-optional, 5 setup-failed. (The suite has no test for the max-stale + must-revalidate combination, so no skip-list entries could be un-skipped; the skippedstale-close-must-revalidatetest depends on the connection-close revalidation path, which is out of scope here.)Notes
handleResult()region as the sibling PR fix: honor request cache-control max-age=0 in cache interceptor #5510 (fix/cache-request-max-age-zero), but both are self-contained against main and merge-independent.🤖 Generated with Claude Code