Skip to content

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

Description

@jeswr

Bug Description

When revalidating a stale entry the interceptor builds the conditional request with

'if-modified-since': new Date(result.cachedAt).toUTCString()

(lib/interceptor/cache.js:354, background path :310) — i.e. the local clock at cache insertion
time
, not the origin's stored Last-Modified value. RFC 9110 §13.1.3 is explicit that caches
should generate If-Modified-Since from the stored response's Last-Modified field. Two concrete
failure modes:

  1. Origins doing exact-match IMS comparison never return 304. nginx's default is
    if_modified_since exact — since cachedAt never equals the resource's Last-Modified, every
    Last-Modified-only revalidation (no ETag) against a default nginx gets a full 200. The entire
    304 bandwidth saving is silently lost for that (very common) origin class.
  2. Clock skew: if the client clock runs ahead of the origin, a resource modified between
    Last-Modified and (client-time) cachedAt can be masked — origin compares its mtime against a
    too-late IMS date and answers 304, so undici serves stale data indefinitely.

Additionally the header is sent even when the stored response never had a Last-Modified at all
(the same expression), which is harmless but meaningless to most origins.

Reproducible By

Server that logs if-modified-since; respond 200 max-age=1, last-modified: <fixed past date>;
request twice with >1s between through a cache-composed dispatcher. Observed: IMS = insertion
wall-clock time, not the fixed Last-Modified (undici 8.7.0, current main, Node 22).

Expected Behavior

Use result.headers['last-modified'] when present; fall back to the stored Date (or omit IMS
and rely on If-None-Match) otherwise. The stored headers are already available on the GetResult
at both call sites, so this is a ~2-line change per site plus tests.


Found during an agent-assisted HTTP-caching review for @jeswr; every claim reproduced on undici 8.6.0 (repo) and 8.7.0 (npm) on Node 22.23.1. Fix PR to follow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions