nodejs / nodejs/undici

cache interceptor ignores request "Cache-Control: max-age=0" (falsy check) — breaks fetch(url, {cache:'no-cache'}) through composed cache dispatchers

Open
#5,504 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
7.7k
Forks
880
Avg merge
2d 16h
Merged PRs (30d)
68

Description

Bug Description

lib/interceptor/cache.js checks the request max-age directive with

if (reqCacheControl?.['max-age'] && age >= reqCacheControl['max-age']) {

(currently interceptor/cache.js:283). For max-age=0 the parsed value 0 is falsy, so the
directive is ignored entirely and a cached response is served with no origin contact.
RFC 9111 §5.2.1.1: the client has indicated it is unwilling to accept any response whose age
exceeds 0s — the cache MUST NOT reuse the stored response without successful validation.

This also breaks fetch(url, { cache: 'no-cache' }) when the dispatcher is composed with the
cache interceptor: per the fetch spec, that mode appends Cache-Control: max-age=0
(lib/web/fetch/index.js, "cache mode is no-cache" step), which the interceptor then drops on the
floor — so the one documented way to force revalidation through fetch does nothing.
(Related umbrella: #3847.)

Secondary issue in the same block: when a nonzero request max-age is exceeded, the bypass
dispatches without a CacheHandler (return dispatch(opts, handler), interceptor/cache.js:286),
so the fresh 200 fetched because of the bypass is never stored — the very next plain request has
to revalidate or refetch again.

Reproducible By

const { Agent, interceptors, cacheStores, request } = require('undici')
const http = require('node:http')

let hits = 0
const server = http.createServer((req, res) => {
  hits++
  res.writeHead(200, { 'cache-control': 'max-age=60', etag: '"v1"' })
  res.end('hello')
}).listen(0, async () => {
  const origin = `http://localhost:${server.address().port}`
  const d = new Agent().compose(interceptors.cache({ store: new cacheStores.MemoryCacheStore() }))
  await (await request(origin, { dispatcher: d })).body.text()                      // hits=1, stored
  await (await request(origin, { dispatcher: d, headers: { 'cache-control': 'max-age=0' } })).body.text()
  console.log(hits) // observed: 1 — expected: 2 (validation required by RFC 9111 §5.2.1.1)
  server.close()
})

Observed on undici 8.7.0 (and current main), Node 22.

Expected Behavior

max-age=0 (and generally age >= max-age) should force the revalidation path (send
If-None-Match/If-Modified-Since, serve cached body on 304), same as the request no-cache
directive already does — not a silent cache hit, and not an un-stored full bypass.

Suggested fix sketch: treat request-max-age exceedance as staleness (feed it into the existing
isStale/revalidation flow) instead of the current falsy-guarded bypass, and wrap the bypass
dispatch in new CacheHandler(...) so the response updates the store.


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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in lib/interceptor/cache.js around the request max-age check and inspect lib/web/fetch/index.js for the no-cache mode behavior. Run the provided composed-dispatcher reproduction against the cache interceptor. Done means max-age=0 performs validation, including conditional 304 handling, and a refreshed response from an exceeded nonzero max-age is stored for the next request.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, nodejs
Domain
api, backend, networking
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.