Skip to content

fix: preserve middleware Vary header in app pages#91039

Open
Felipeness wants to merge 1 commit intovercel:canaryfrom
Felipeness:fix/vary-header-overwrite
Open

fix: preserve middleware Vary header in app pages#91039
Felipeness wants to merge 1 commit intovercel:canaryfrom
Felipeness:fix/vary-header-overwrite

Conversation

@Felipeness
Copy link

Summary

Fixes #85999

The handler function in the app page template (app-page.ts) used res.setHeader('Vary', varyHeader) which overwrote any Vary headers previously set by middleware or by base-server.ts's setVaryHeader(). This caused CDN cache issues because custom Vary values added by middleware (e.g. Vary: X-Foo) were silently discarded, leading to incorrect cache behavior.

Root Cause

The request lifecycle for app pages is:

  1. Middleware sets custom Vary headers (e.g. Vary: X-Foo) via response.headers.append('vary', ...)
  2. base-server.ts:setVaryHeader() correctly uses res.appendHeader('vary', ...) to add RSC headers without overwriting
  3. app-page.ts:handler() calls res.setHeader('Vary', varyHeader) which overwrites everything from steps 1 and 2

The handler receives the raw ServerResponse (not NodeNextResponse), so setHeader is a complete replacement.

Fix

Instead of blindly overwriting, the fix reads the existing Vary header, merges it with the RSC values using case-insensitive deduplication (via Set), and sets the combined result. This follows the same pattern used in send-response.ts which already treats vary as a multi-value header, and the deduplication pattern from patch-set-header.ts for Set-Cookie.

The same fix is applied to edge-ssr-app.ts which had an equivalent issue with headers.set('Vary', varyHeader).

Changes

  • packages/next/src/build/templates/app-page.ts: Replace res.setHeader('Vary', varyHeader) with read-merge-set logic that preserves existing Vary values
  • packages/next/src/build/templates/edge-ssr-app.ts: Add Vary-aware merging in the existing headers loop to avoid overwriting RSC Vary values
  • test/e2e/vary-header/: Add app page test fixture and test case verifying middleware Vary headers are preserved in app pages

Before (broken)

Middleware sets: Vary: X-Foo
base-server appends: Vary: X-Foo, RSC, Next-Router-State-Tree, ...
app-page.ts overwrites: Vary: RSC, Next-Router-State-Tree, ...  ← X-Foo lost!

After (fixed)

Middleware sets: Vary: X-Foo
base-server appends: Vary: X-Foo, RSC, Next-Router-State-Tree, ...
app-page.ts merges: Vary: X-Foo, RSC, Next-Router-State-Tree, ...  ← X-Foo preserved!

The `handler` in `app-page.ts` used `res.setHeader('Vary', ...)` which
overwrote any Vary headers previously set by middleware or
`base-server.ts`'s `setVaryHeader()`. This caused CDN cache issues when
middleware added custom Vary values (e.g. `Vary: X-Foo`), as those
values were silently discarded.

The fix reads existing Vary values, merges them with the RSC headers
using case-insensitive deduplication, and sets the combined result.
The same pattern is applied to the edge runtime template
(`edge-ssr-app.ts`) which had an equivalent issue with `headers.set()`.

Closes vercel#85999
@nextjs-bot
Copy link
Collaborator

Allow CI Workflow Run

  • approve CI run for commit: 1e279ba

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

@Felipeness
Copy link
Author

Alternative approaches considered

Before settling on the read-merge-set approach, I considered two other alternatives:

1. Remove the setHeader entirely from app-page.ts

Since base-server.ts's setVaryHeader() already correctly uses appendHeader and runs before the handler, one option was to simply remove the res.setHeader('Vary', varyHeader) line from the template. However, the handler is designed to be self-contained — it can be invoked directly through other code paths (e.g. next-server.ts for minimal mode) that may not call setVaryHeader() first. Removing it could result in missing Vary headers in those paths.

2. Simple appendHeader instead of setHeader

Changing res.setHeader to res.appendHeader would be a minimal one-line fix. The problem is that the res here is a raw Node.js ServerResponse, whose appendHeader does not deduplicate values (unlike NodeNextResponse.appendHeader which does). Since base-server.ts already appends the same RSC headers before the handler runs, this would cause duplicated Vary values like RSC, Next-Router-State-Tree, RSC, Next-Router-State-Tree, ....

Why read-merge-set

The chosen approach reads existing Vary values, deduplicates case-insensitively via Set, and sets the merged result. This follows the same pattern used in patch-set-header.ts for Set-Cookie deduplication and aligns with send-response.ts which already treats vary as a multi-value header.

Happy to adjust the approach if the team prefers a different direction!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vary header is being overwritten by Next causing CDN issues

2 participants