#3318·vinext

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

typescript
// 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)

  1. applyCdnResponseHeaders (server/cache-control.js) unconditionally deletes Cache-Control before stamping framework/adapter policy, so middleware set() semantics from mergeMiddlewareResponseHeaders (PR #789) cannot survive on pages served through the finalizer path.
  2. finalizeGatewayResponse (@vinext/cloudflare/dist/cache/cdn-adapter.worker.js) strips CDN-Cache-Control/Cache-Tag and forces browser private, max-age=0, must-revalidate on shared-stage responses. Deliberate per the comment, but it means middleware cache policy has no egress path at all.
  3. hasTaggedCustomVary + line 313: a response with Cache-Tag and any non-framework Vary field is routed to preventResponseCaching() (forced no-store). So a custom Vary (e.g. Vary: Host for 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: Host is the documented Workers Caching mechanism for host-based partitioning (all Vary names honored). Its interaction with tag purges (variants share purge identity) is a genuine conflict, but failing closed to no-store with no signal leaves multi-host Workers (i18n domains, white-label tenants) with no supported partitioning story: the key has no host, Vary: Host kills caching, and ctx.props/cf.cacheKey don't apply to eyeball traffic through a single entrypoint.

Ask

  1. Dev-time warning when middleware sets cache-ownership headers (Cache-Control, CDN-Cache-Control, Cache-Tag) or custom Vary under an active CDN adapter that will discard/override them.
  2. Document the supported host-partitioning story for single-entrypoint Workers (or confirm x-forwarded-host keying as the sanctioned mechanism).
  3. Consider: instead of preventResponseCaching on 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.