Summary
Several adapters only set Vary: Accept on the HTML response as part of the Link-header injection. When the Link header is turned off (enableLinkHeader: false, or injectLinkHeader: false for nextjs/sveltekit), the negotiable HTML response goes out with no Vary header at all.
Spec
content-negotiation.md section 3:
A conformant server MUST set Vary: Accept on every response whose representation depends on the Accept header (i.e. every page that has both an HTML and markdown form).
That requirement is independent of whether the markdown twin is advertised via a Link header. Disabling the Link header does not stop the page from being content-negotiated, so Vary: Accept is still required.
Impact
A shared/CDN cache keyed on the URL alone can store the HTML response and later serve it to a client that sent Accept: text/markdown (or a bot), and vice versa. This is a cache-correctness bug, not just a missing header.
Where
The Vary append is nested inside the Link-header block (or gated by the same flag):
packages/cloudflare/src/worker.ts (block gated by enableLinkHeader &&)
packages/netlify/src/worker.ts (same)
packages/vercel/src/middleware.ts (same)
packages/nextjs/src/middleware.ts (appendVaryAccept is inside if (injectLinkHeader))
packages/sveltekit/src/handle.ts (if (!injectLinkHeader) return response before any Vary handling)
packages/deno/src/handler.ts and packages/fastly/src/handler.ts already do this correctly: they always set Vary: Accept and gate only the Link line.
Reproduction
import { createAEOWorker } from "@dualmark/cloudflare";
const worker = createAEOWorker({ upstream, enableLinkHeader: false });
// GET /page (browser UA, Accept: text/html), upstream returns text/html
// actual: Vary header is null
// expected: Vary: Accept
Proposed fix
Always run the Vary: Accept append on the negotiable HTML branch; gate only the Link header line on the flag, matching deno/fastly. PR incoming.
Summary
Several adapters only set
Vary: Accepton the HTML response as part of the Link-header injection. When the Link header is turned off (enableLinkHeader: false, orinjectLinkHeader: falsefor nextjs/sveltekit), the negotiable HTML response goes out with noVaryheader at all.Spec
content-negotiation.md section 3:
That requirement is independent of whether the markdown twin is advertised via a
Linkheader. Disabling the Link header does not stop the page from being content-negotiated, soVary: Acceptis still required.Impact
A shared/CDN cache keyed on the URL alone can store the HTML response and later serve it to a client that sent
Accept: text/markdown(or a bot), and vice versa. This is a cache-correctness bug, not just a missing header.Where
The
Varyappend is nested inside the Link-header block (or gated by the same flag):packages/cloudflare/src/worker.ts(block gated byenableLinkHeader &&)packages/netlify/src/worker.ts(same)packages/vercel/src/middleware.ts(same)packages/nextjs/src/middleware.ts(appendVaryAcceptis insideif (injectLinkHeader))packages/sveltekit/src/handle.ts(if (!injectLinkHeader) return responsebefore any Vary handling)packages/deno/src/handler.tsandpackages/fastly/src/handler.tsalready do this correctly: they always setVary: Acceptand gate only theLinkline.Reproduction
Proposed fix
Always run the
Vary: Acceptappend on the negotiable HTML branch; gate only theLinkheader line on the flag, matching deno/fastly. PR incoming.