fix: update stored cache entry on synchronous 304 revalidation#5516
Open
jeswr wants to merge 3 commits into
Open
fix: update stored cache entry on synchronous 304 revalidation#5516jeswr wants to merge 3 commits into
jeswr wants to merge 3 commits into
Conversation
A 304 validation response on the synchronous revalidation path was swallowed by CacheRevalidationHandler before the CacheHandler could see it, so the stored entry was never updated per RFC 9111 §4.3.4: headers carried by the 304 (e.g. a new Cache-Control) were discarded and the entry's freshness was never recomputed, causing a revalidation request on every single subsequent use, forever. The served response also kept the obsolete `Warning: 110` header even though it had just been successfully validated. - CacheHandler now handles 304s before the cacheability gates and recomputes freshness from the merged (stored + 304) headers, so bare 304s also refresh the entry; the stored Vary and ETag are preserved. - CacheRevalidationHandler forwards 304s to a dedicated cache-update CacheHandler and passes the validation response's headers to the callback, so the interceptor serves the freshened headers with age 0 and no stale warning. - The 304 body-reuse path no longer byte-iterates Buffer bodies returned by SqliteCacheStore (Buffer.prototype.values() yields single bytes). Un-skips four 304-etag-update-response-* conformance tests; 304-etag-update-response-Cache-Control remains skipped as it needs an entry-retention floor (the max-age=1 entry is deleted before the test's 3s pause; see nodejs#4624). Fixes nodejs#5505 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5516 +/- ##
==========================================
- Coverage 93.44% 93.43% -0.02%
==========================================
Files 110 110
Lines 37328 37486 +158
==========================================
+ Hits 34881 35024 +143
- Misses 2447 2462 +15 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
b772c81 to
76b0d67
Compare
…ils on main) 'stale-while-revalidate updates cache after background revalidation (receiving new data)' failed on Windows/Node 26 with revalidationRequests 0 == 1. The identical failure (same test, same assertion at test/interceptors/cache.js:1604) occurred on main's own CI on Windows/Node 22 (run 28649055741, 2026-07-03) without this change. The test passes 10/10 locally on this branch and on main, the sibling SWR test with the same flow passed in the failing job, and Windows/Node 24 passed on this same commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collapse the multi-line prose comments introduced with the 304 freshening change to single-line "why" comments, matching the terse style of the surrounding cache interceptor. Addresses review feedback on comment verbosity; no logic change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
76b0d67 to
24642c2
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 #5505
Problem
#4617 (fixing #4596) taught
CacheHandlerto merge 304 responses into the stored entry, but that code path was only reachable from the stale-while-revalidate background refresh. On the synchronous revalidation path,CacheRevalidationHandlershort-circuited onstatusCode === 304and never forwarded anything to the wrappedCacheHandler, so:Cache-Control) were discarded. RFC 9111 §4.3.4 says the cache MUST update the stored response with the 304's header fields and recompute freshness. An origin extending freshness via304 + Cache-Control: max-age=60got zero benefit: undici revalidated again on the very next request, every time, forever.Warning: 110 - "response is stale"although it had just been successfully validated (TODO inlib/interceptor/cache.js; theWarningheader is also obsolete per RFC 9111 §5.5).Additionally,
CacheHandler's cacheability pre-checks ran against the 304's own headers, so bare 304s bypassed the merge branch even on the background path, and the merged entry lost its storedVary.Repro
Before (main @ cb4c2f1):
hits = 4and keeps growing on every request; validated response served withwarning: 110 - "response is stale"and the oldcache-control: max-age=1.After:
hits = 2; validated response served with nowarning, the mergedcache-control: max-age=60, andage: 0; subsequent requests are cache hits.Changes
lib/handler/cache-handler.js: 304 handling moved in front of the cacheability gates into a#handle304method. Freshness (staleAt/deleteAt/cachedAt) is recomputed from the merged (stored + 304) headers per §4.3.4, so bare 304s also refresh the entry; the storedVaryandETagare preserved (an ETag on the 304 wins if usable);Content-Lengthfrom the 304 is not merged. New exported helpermergeValidationHeaders.lib/handler/cache-revalidation-handler.js: on a 304 the response events are forwarded to a dedicated cache-updateCacheHandler(silent downstream), and the validation response's status/headers are passed to the callback.lib/interceptor/cache.js: on successful validation the cached body is served with the freshened headers,age: 0, and without theWarning: 110header (still emitted for stale-if-error 5xx fallbacks and stale-while-revalidate serves, which really are stale).Bufferbodies returned bySqliteCacheStore(Buffer.prototype.values()yields single bytes, corrupting the re-written entry).Tests
test/interceptors/cache-revalidate-stale.js— "304 revalidation response updates the stored entry (RFC 9111 §4.3.4)" and "bare 304 revalidation response refreshes the stored entry". Both fail on main, pass with this change.test/interceptors/cache*.js,test/cache-interceptor/*.js): 123/123 pass (121 baseline + 2 new).node test/cache-interceptor/cache-tests.mjs): un-skipped four304-etag-update-response-*tests ("We're not caching 304s currently"); they now pass in all 4 environments (shared/private × MemoryCacheStore/SqliteCacheStore). Totals: Passed 220 → 238, Failed 0 → 0, Failed-optional 58 → 44 (14 optionalupdate304checks flipped to yes), Setup-failed 5 → 5. A per-test diff against main shows every status change is within theupdate304family; nothing else moved.304-etag-update-response-Cache-Controlstays skipped with an updated comment: its stored entry (max-age=1) is deleted at ~2x its freshness lifetime, before the test's 3s pause, so there is nothing left to revalidate — that needs the entry-retention floor discussed in fix: max-age=0 and no-cache must be considered "immediately stale" #4624 (fix/cache-store-etag-no-cacheterritory), not 304 merging.Related
Self-contained against
main; sibling PRs from the same review touch adjacent logic:jeswr:fix/cache-if-modified-since-value(revalidation request headers),jeswr:fix/cache-request-max-age-zero(request-directive revalidation path),jeswr:fix/cache-store-etag-no-cache(entry retention for revalidation-only responses).This change is agent-assisted (Claude Fable 5) working with @jeswr.