Skip to content

Stop the fetch cache option from overriding the origin's Cache-Control - #7749

Merged
danlapid merged 1 commit into
mainfrom
dlapid/cache-no-cache-spec
Oct 14, 2026
Merged

danlapid merged 1 commit into
mainfrom
dlapid/cache-no-cache-spec

Conversation

@danlapid

@danlapid danlapid commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Per the Fetch spec, a request's cache mode describes how the cache may use a stored response: no-cache means revalidate it with the origin, no-store means act as if there is no cache. Neither says anything about how the response may be cached. Today serializeCfBlobJson() also synthesizes cf.cacheControl from the cache option:

RequestInit.cache Without flag With flag
no-cache cacheForceRevalidate: true, cacheControl: "no-cache" cacheForceRevalidate: true
no-store cacheTtl: -1, cacheLevel: "bypass", cacheControl: "no-store" cacheTtl: -1, cacheLevel: "bypass"

The synthesized cacheControl overrides the origin's response Cache-Control. An origin no-store gets cached as no-cache, which breaks customers who only want forced revalidation.

This adds the cache_option_preserves_cache_control compat flag (disable flag cache_option_overrides_cache_control, default on 2026-11-17). When it's on, the cache option no longer synthesizes cacheControl. cacheControl synthesized from cf.cacheTtl is unchanged, since cacheTtl is meant to override the origin's response. Overriding the origin's Cache-Control stays available through cf.cacheControl.

The synthesis predates cf.cacheControl (dd099be), so changing it requires a compat flag.

Questions for reviewers

  1. Enable date. I used 2026-11-17 to match the latest dated flag. Let me know if it should be different.
  2. cf.cacheControl with the cache option. RequestInitializerDict::validate() still rejects using both together. With this flag, they are separate controls, so we may want to allow the pair when the flag is on. I left that out of this PR.
  3. cache: 'no-store' and cacheTtl: -1. With the flag, no-store still sets cacheTtl: -1 along with cacheLevel: bypass. If downstream reads cacheTtl: -1 as an override of the origin's response, no-store should probably set only the cache level. Please flag it if so.

Testing

cf-cache-control-test now has a service binding that echoes the request.cf it receives. Two services pin the flag on and off, and the test checks the serialized blob for cache: 'no-cache', cache: 'no-store' and cf.cacheTtl. All three variants of cf-cache-control-test pass, as do http-test@ and http-test@all-compat-flags.

🤖 Generated with Claude Code

@danlapid
danlapid requested review from a team as code owners October 14, 2026 21:21
@ask-bonk

ask-bonk Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Since last review: 0 resolved, 0 still open, 1 new.
LGTM!

The cache-option compat flag now preserves origin cache-control behavior and tests both flag states.

Other findings

  • [SUGGESTION] src/workerd/api/http.c++:692: The two comments immediately above this conditional still promise that every cache mode synthesizes and dual-writes cacheControl, but the enabled flag deliberately leaves it unset. Update them to describe the conditional path.

    // Synthesize cacheControl from cacheTtl, or from cacheMode for legacy behavior, when
    // cacheControl is not explicitly set.
    

    Write comments that describe the current state of the system, not the task, change, or debugging that produced the code.

    — AGENTS.md


Reviewed commit: 59a83298 · github run

Comment thread src/workerd/io/compatibility-date.capnp Outdated
@danlapid
danlapid force-pushed the dlapid/cache-no-cache-spec branch from 264bcb4 to eff62aa Compare October 14, 2026 21:34
Per the Fetch spec, a request's cache mode describes how the cache may
use a stored response: "no-cache" means revalidate it with the origin,
"no-store" means act as if there is no cache. Neither says anything
about how the response may be cached. serializeCfBlobJson() currently
also synthesizes `cf.cacheControl: "no-cache"` / `"no-store"` from
`cache: 'no-cache'` / `cache: 'no-store'`, which overrides the origin's
response Cache-Control. An origin `no-store` gets cached as `no-cache`,
which breaks customers who only want forced revalidation.

The new `cache_option_preserves_cache_control` compat flag (default on
2026-11-17) stops the cache option from synthesizing cacheControl.
`cache: 'no-cache'` still sets cacheForceRevalidate; `cache: 'no-store'`
still sets cacheTtl -1 and cacheLevel bypass. cacheControl synthesized
from cf.cacheTtl is unchanged, since cacheTtl is meant to override the
origin's response. Overriding the origin's Cache-Control stays available
through `cf.cacheControl`.

Compatibility: changes behavior of an existing API, so it is gated
behind a compat flag.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@danlapid
danlapid force-pushed the dlapid/cache-no-cache-spec branch from eff62aa to 59a8329 Compare October 14, 2026 22:16
@danlapid danlapid changed the title Stop cache: 'no-cache' from overriding the origin's Cache-Control Stop the fetch cache option from overriding the origin's Cache-Control Oct 14, 2026
@danlapid
danlapid merged commit f735b36 into main Oct 14, 2026
26 of 27 checks passed
@danlapid
danlapid deleted the dlapid/cache-no-cache-spec branch October 14, 2026 22:49
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.

2 participants