Problem
Several resource responses vary by request inputs, but the API emits no Vary and no Cache-Control on them. That leaves every cache in front of the API — CDN, reverse proxy, and especially a browser service worker — unable to tell a cache-safe (public) response from an unsafe (auth-/context-specific) one, because there is no signal on the response and often no distinguishing URL.
This surfaced while designing opt-in PWA/offline support for the Nuxt module (cwa-nuxt-module#258). The edge cache (Souin) already handles this correctly by excluding any request carrying the api_component cookie — so production is safe today. The ask here is to make that same safety expressible in the response itself, so any downstream cache can honour it without out-of-band configuration.
The two dimensions responses vary on
1. Auth cookie (draft vs published) — same URL, different body
GET /_/routes/{path} and GET /_/resource_manifest/{path} carry no ?published= marker — a draft and a published page are served from an identical URL, chosen by the api_component cookie.
ComponentPosition (not itself publishable): ComponentPositionNormalizer::normalize rewrites $object->component to the draft IRI for a permitted user and the published IRI otherwise — same /_/component_positions/{id} URL. It also serialises pageDataProperty under the ComponentPosition:read:role_admin group. So the body varies by role.
2. The path request header (dynamic positions) — same URL, different body
A dynamic ComponentPosition resolves its component against page data keyed off the path request header (PageDataProvider::getOriginalRequestPath() → headers->get('path')). So the same position IRI returns different components depending on the header — a second, independent cache-key dimension that no cache currently knows about.
What would help significantly
Emit, on responses whose body depends on these inputs:
Vary naming the inputs the body depends on — at minimum Cookie, and for resources that resolve against the path header, whatever header carries it (currently the custom path header — a Vary: path or a documented equivalent). This lets a correct shared cache key on those responses, instead of silently serving one variant for all.
Cache-Control stating intent explicitly — e.g. private, no-store (or no-cache) when a response is auth-specific, and a cacheable directive when it is genuinely public. Right now the absence of any directive means each cache guesses.
A single response-header marker for "this response was personalised / auth-scoped" would let a service worker's cacheWillUpdate drop unsafe responses by construction — the SW cache would then only ever hold public data, matching the rule Souin already enforces at the edge. Without it, front-end caching has to rely on URL denylists (impossible here — no distinguishing URL) or on mitigations (purge-on-logout, purge-on-401, short TTL) that are strictly weaker than an authoritative header.
Why this matters beyond the service worker
The Vary gap is a latent correctness issue for any shared cache placed in front of the API, not just the SW — the SW is simply the case that forced the analysis. Souin is configured correctly today, but that safety lives in edge config, not in the responses, so it does not travel with them.
Notes / scope
- Souin's existing cookie-exclusion is the right behaviour and should stay; this is about making the same decision legible to other caches via standard headers.
- Front-end (module) side is tracked in cwa-nuxt-module#258 — it can only safely add SW API caching once a cache-safety marker +
Vary exist here.
- Filed from the Nuxt-module side; happy to adjust the exact header names/semantics to whatever fits the bundle's conventions.
Problem
Several resource responses vary by request inputs, but the API emits no
Varyand noCache-Controlon them. That leaves every cache in front of the API — CDN, reverse proxy, and especially a browser service worker — unable to tell a cache-safe (public) response from an unsafe (auth-/context-specific) one, because there is no signal on the response and often no distinguishing URL.This surfaced while designing opt-in PWA/offline support for the Nuxt module (cwa-nuxt-module#258). The edge cache (Souin) already handles this correctly by excluding any request carrying the
api_componentcookie — so production is safe today. The ask here is to make that same safety expressible in the response itself, so any downstream cache can honour it without out-of-band configuration.The two dimensions responses vary on
1. Auth cookie (draft vs published) — same URL, different body
GET /_/routes/{path}andGET /_/resource_manifest/{path}carry no?published=marker — a draft and a published page are served from an identical URL, chosen by theapi_componentcookie.ComponentPosition(not itself publishable):ComponentPositionNormalizer::normalizerewrites$object->componentto the draft IRI for a permitted user and the published IRI otherwise — same/_/component_positions/{id}URL. It also serialisespageDataPropertyunder theComponentPosition:read:role_admingroup. So the body varies by role.2. The
pathrequest header (dynamic positions) — same URL, different bodyA dynamic
ComponentPositionresolves itscomponentagainst page data keyed off thepathrequest header (PageDataProvider::getOriginalRequestPath()→headers->get('path')). So the same position IRI returns different components depending on the header — a second, independent cache-key dimension that no cache currently knows about.What would help significantly
Emit, on responses whose body depends on these inputs:
Varynaming the inputs the body depends on — at minimumCookie, and for resources that resolve against the path header, whatever header carries it (currently the custompathheader — aVary: pathor a documented equivalent). This lets a correct shared cache key on those responses, instead of silently serving one variant for all.Cache-Controlstating intent explicitly — e.g.private, no-store(orno-cache) when a response is auth-specific, and a cacheable directive when it is genuinely public. Right now the absence of any directive means each cache guesses.A single response-header marker for "this response was personalised / auth-scoped" would let a service worker's
cacheWillUpdatedrop unsafe responses by construction — the SW cache would then only ever hold public data, matching the rule Souin already enforces at the edge. Without it, front-end caching has to rely on URL denylists (impossible here — no distinguishing URL) or on mitigations (purge-on-logout, purge-on-401, short TTL) that are strictly weaker than an authoritative header.Why this matters beyond the service worker
The
Varygap is a latent correctness issue for any shared cache placed in front of the API, not just the SW — the SW is simply the case that forced the analysis. Souin is configured correctly today, but that safety lives in edge config, not in the responses, so it does not travel with them.Notes / scope
Varyexist here.