Skip to content

fix: send stored last-modified as if-modified-since when revalidating#5512

Open
jeswr wants to merge 2 commits into
nodejs:mainfrom
jeswr:fix/cache-if-modified-since-value
Open

fix: send stored last-modified as if-modified-since when revalidating#5512
jeswr wants to merge 2 commits into
nodejs:mainfrom
jeswr:fix/cache-if-modified-since-value

Conversation

@jeswr

@jeswr jeswr commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

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 in lib/interceptor/cache.js.

RFC 9110 §13.1.3 says a cache should generate If-Modified-Since from the stored response's Last-Modified field value, falling back to the Date field value, and only then the local time the response was cached.

Concrete failure modes of the old behavior:

  1. Exact-match origins never return 304. nginx's default is if_modified_since exact; since the local insertion time never equals the resource's Last-Modified, every Last-Modified-only revalidation (no ETag) against such origins gets a full 200, silently losing the 304 bandwidth saving.
  2. Clock skew can serve stale data. If the client clock runs ahead of the origin, a resource modified between Last-Modified and (client-time) cachedAt is masked: the origin compares its mtime against a too-late date and answers 304.

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's if-modified-since:

  • Before (main): If-Modified-Since: <cache insertion wall-clock time> (e.g. Wed, 02 Jul 2026 ... GMT) — does not match the stored Last-Modified.
  • After (this PR): If-Modified-Since: Sat, 09 Oct 2010 14:28:02 GMT — the stored value, verbatim.

Changes

  • Add a getIfModifiedSinceValue(result) helper implementing the RFC 9110 §13.1.3 hierarchy (stored last-modified → stored datecachedAt) and use it at both revalidation call sites. If-None-Match behavior is unchanged.
  • Tests in test/interceptors/cache-revalidate-stale.js: verbatim Last-Modified on the synchronous revalidation path, on the stale-while-revalidate background path, and the Date-header fallback when the stored response has no Last-Modified. All three fail on main and pass with this change.

Tests

  • node --test over 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).
  • mnot cache-tests conformance runner (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

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>
Copilot AI review requested due to automatic review settings July 4, 2026 16:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov-commenter

codecov-commenter commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.45%. Comparing base (cb4c2f1) to head (efe48e3).

Files with missing lines Patch % Lines
lib/interceptor/cache.js 90.00% 2 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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>
@jeswr jeswr force-pushed the fix/cache-if-modified-since-value branch from 58a0374 to efe48e3 Compare July 4, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cache: revalidation sends If-Modified-Since derived from local insertion time instead of the stored Last-Modified (RFC 9110 §13.1.3)

3 participants