fix: send stored last-modified as if-modified-since when revalidating#5512
Open
jeswr wants to merge 2 commits into
Open
fix: send stored last-modified as if-modified-since when revalidating#5512jeswr wants to merge 2 commits into
jeswr wants to merge 2 commits into
Conversation
When revalidating a stale cached response, the cache interceptor built
the conditional request with
'if-modified-since': new Date(result.cachedAt).toUTCString()
i.e. the local wall-clock time the response was inserted into the
cache, rather than the stored response's Last-Modified value. RFC 9110
Section 13.1.3 says caches should generate If-Modified-Since from the
stored Last-Modified field, falling back to the Date field or the local
receipt time.
Consequences of the old behavior:
- Origins doing exact-match If-Modified-Since comparison (e.g. nginx
default `if_modified_since exact`) never answer 304 to a
Last-Modified-only revalidation, losing the 304 bandwidth saving.
- If the client clock runs ahead of the origin, a resource modified
between Last-Modified and (client-time) cachedAt can be masked and
stale data served indefinitely.
Now the revalidation request (both the synchronous path and the
stale-while-revalidate background path) sends the stored last-modified
value verbatim when present, then falls back to the stored date value,
and only uses the local insertion time when the stored response has
neither.
Fixes nodejs#5506
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5512 +/- ##
=======================================
Coverage 93.44% 93.45%
=======================================
Files 110 110
Lines 37328 37346 +18
=======================================
+ Hits 34881 34901 +20
+ Misses 2447 2445 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
7 tasks
Trim the getIfModifiedSinceValue JSDoc to a single-line summary plus the RFC reference, addressing review feedback favouring terse interceptor comments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
58a0374 to
efe48e3
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 #5506
What
When revalidating a stale cached response, the cache interceptor built the conditional request with
'if-modified-since': new Date(result.cachedAt).toUTCString()— the local wall-clock time the response was inserted into the cache — on both the synchronous revalidation path and the stale-while-revalidate background path inlib/interceptor/cache.js.RFC 9110 §13.1.3 says a cache should generate
If-Modified-Sincefrom the stored response'sLast-Modifiedfield value, falling back to theDatefield value, and only then the local time the response was cached.Concrete failure modes of the old behavior:
if_modified_since exact; since the local insertion time never equals the resource'sLast-Modified, everyLast-Modified-only revalidation (no ETag) against such origins gets a full200, silently losing the 304 bandwidth saving.Last-Modifiedand (client-time)cachedAtis masked: the origin compares its mtime against a too-late date and answers304.Repro (before → after)
Origin responding
Cache-Control: max-age=1+Last-Modified: Sat, 09 Oct 2010 14:28:02 GMT, two requests >1s apart through a cache-composed dispatcher, logging the revalidation request'sif-modified-since:If-Modified-Since: <cache insertion wall-clock time>(e.g.Wed, 02 Jul 2026 ... GMT) — does not match the storedLast-Modified.If-Modified-Since: Sat, 09 Oct 2010 14:28:02 GMT— the stored value, verbatim.Changes
getIfModifiedSinceValue(result)helper implementing the RFC 9110 §13.1.3 hierarchy (storedlast-modified→ storeddate→cachedAt) and use it at both revalidation call sites.If-None-Matchbehavior is unchanged.test/interceptors/cache-revalidate-stale.js: verbatimLast-Modifiedon the synchronous revalidation path, on the stale-while-revalidate background path, and theDate-header fallback when the stored response has noLast-Modified. All three fail onmainand pass with this change.Tests
node --testover the cache interceptor test files (test/interceptors/cache*.js,test/cache-interceptor/*): 124 tests / 13 suites, 124 pass, 0 fail (baseline on main: 121/121 + the 3 new tests).node test/cache-interceptor/cache-tests.mjs): Total 283 — Passed 222, Failed 0, Failed-optional 57, Setup failed 4 (equal or better than the main baseline of 220 / 0 / 58 / 5; the conformance suite has no If-Modified-Since-generation test, so no skip-list entries could be un-skipped by this change).Notes
This change is agent-assisted (Claude Fable 5) working with @jeswr. It is part of a series of self-contained cache-interceptor conformance fixes against
main; sibling PRs touching adjacent logic in the same interceptor: #5510 (fix/cache-request-max-age-zero) and #5511 (fix/cache-must-revalidate-max-stale).🤖 Generated with Claude Code