cache.cdn (Cloudflare): middleware response headers silently discarded; custom Vary forces no-store
Author: viztorCreated Sep 18, 2026Updated Sep 18, 2026
Summary
When the Cloudflare CDN cache adapter is configured (vinext({ cache: { cdn: cdnAdapter() } })), response headers set in middleware (proxy.ts/middleware.ts) never reach the client on App Router page responses, with no warning. Separately, any custom (non-framework) Vary field on a tagged response forces the response to no-store.
Observed on [email protected] + @vinext/[email protected], Cloudflare Workers production deployment.
Repro
// proxy.ts
export default function proxy(request: NextRequest) {
const rewritten = NextResponse.rewrite(url);
rewritten.headers.set("Cache-Control", "public, max-age=60");
rewritten.headers.append("Vary", "Host");
return rewritten;
}Actual response headers: Cache-Control: private, max-age=0, must-revalidate, Vary contains only the framework RSC fields. The middleware values vanish.
Mechanism (from dist source)
applyCdnResponseHeaders(server/cache-control.js) unconditionallydeletesCache-Controlbefore stamping framework/adapter policy, so middlewareset()semantics frommergeMiddlewareResponseHeaders(PR #789) cannot survive on pages served through the finalizer path.finalizeGatewayResponse(@vinext/cloudflare/dist/cache/cdn-adapter.worker.js) stripsCDN-Cache-Control/Cache-Tagand forces browserprivate, max-age=0, must-revalidateon shared-stage responses. Deliberate per the comment, but it means middleware cache policy has no egress path at all.hasTaggedCustomVary+ line 313: a response withCache-Tagand any non-frameworkVaryfield is routed topreventResponseCaching()(forcedno-store). So a customVary(e.g.Vary: Hostfor multi-tenant/host-based partitioning, where the host is intentionally not in the Workers Cache key) doesn't just get dropped — it actively disables edge caching for the response.
Why this matters
- Silent: nothing warns that middleware cache headers are dead under
cache.cdn. The Next.js mental model (middleware owns singular headers) from PR #789 holds for route handlers but not for pages through the two-stage gateway + cached-entrypoint path. Vary: Hostis the documented Workers Caching mechanism for host-based partitioning (allVarynames honored). Its interaction with tag purges (variants share purge identity) is a genuine conflict, but failing closed tono-storewith no signal leaves multi-host Workers (i18n domains, white-label tenants) with no supported partitioning story: the key has no host,Vary: Hostkills caching, andctx.props/cf.cacheKeydon't apply to eyeball traffic through a single entrypoint.
Ask
- Dev-time warning when middleware sets cache-ownership headers (
Cache-Control,CDN-Cache-Control,Cache-Tag) or customVaryunder an active CDN adapter that will discard/override them. - Document the supported host-partitioning story for single-entrypoint Workers (or confirm
x-forwarded-hostkeying as the sanctioned mechanism). - Consider: instead of
preventResponseCachingon tagged+custom-Vary, document the trade-off and let the adapter's risk call it (or scope tags per variant), so host-partitioned caching stays possible.
Source: cloudflare/vinext