Repository: vercel/next.js
Pull request: Revert "[next-server] skip setting vary header for basic routes"

Reverts vercel/next.js#77797
Reinstate the `Vary` header to prevent browsers from incorrectly reusing RSC responses during bfcache restores.

Changed files:
filename: packages/next/src/server/base-server.ts
patch: @@ -104,6 +104,7 @@ import {
   NEXT_ROUTER_SEGMENT_PREFETCH_HEADER,
   NEXT_DID_POSTPONE_HEADER,
   NEXT_URL,
+  NEXT_ROUTER_STATE_TREE_HEADER,
   NEXT_IS_PRERENDER_HEADER,
 } from '../client/components/app-router-headers'
 import type {
@@ -2020,16 +2021,21 @@ export default abstract class Server<
     isAppPath: boolean,
     resolvedPathname: string
   ): void {
+    const baseVaryHeader = `${RSC_HEADER}, ${NEXT_ROUTER_STATE_TREE_HEADER}, ${NEXT_ROUTER_PREFETCH_HEADER}, ${NEXT_ROUTER_SEGMENT_PREFETCH_HEADER}`
+    const isRSCRequest = getRequestMeta(req, 'isRSCRequest') ?? false
+
     let addedNextUrlToVary = false
 
     if (isAppPath && this.pathCouldBeIntercepted(resolvedPathname)) {
       // Interception route responses can vary based on the `Next-URL` header.
       // We use the Vary header to signal this behavior to the client to properly cache the response.
-      res.appendHeader('vary', `${NEXT_URL}`)
+      res.appendHeader('vary', `${baseVaryHeader}, ${NEXT_URL}`)
       addedNextUrlToVary = true
+    } else if (isAppPath || isRSCRequest) {
+      // We don't need to include `Next-URL` in the Vary header for non-interception routes since it won't affect the response.
+      // We also set this header for pages to avoid caching issues when navigating between pages and app.
+      res.appendHeader('vary', baseVaryHeader)
     }
-    // For other cases such as App Router requests or RSC requests we don't need to set vary header since we already
-    // have the _rsc query with the unique hash value.
 
     if (!addedNextUrlToVary) {
       // Remove `Next-URL` from the request headers we determined it wasn't necessary to include in the Vary header.

filename: test/e2e/app-dir/app/index.test.ts
patch: @@ -323,6 +323,29 @@ describe('app dir - basic', () => {
     expect(res.headers.get('Content-Type')).toBe('text/x-component')
   })
 
+  it('should return the `vary` header from edge runtime', async () => {
+    const res = await next.fetch('/dashboard')
+    expect(res.headers.get('x-edge-runtime')).toBe('1')
+    expect(res.headers.get('vary')).toBe(
+      'RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch'
+    )
+  })
+
+  if (!isNextDeploy) {
+    it('should return the `vary` header from pages for flight requests', async () => {
+      const res = await next.fetch('/', {
+        headers: {
+          ['RSC'.toString()]: '1',
+        },
+      })
+      expect(res.headers.get('vary')).toBe(
+        isNextDeploy
+          ? 'RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch'
+          : 'RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Accept-Encoding'
+      )
+    })
+  }
+
   it('should pass props from getServerSideProps in root layout', async () => {
     const $ = await next.render$('/dashboard')
     expect($('title').first().text()).toBe('hello world')

filename: test/e2e/vary-header/test/index.test.ts
patch: @@ -12,16 +12,21 @@ describe('Vary Header Tests', () => {
     expect(res.headers.get('vary')).toContain('Custom-Header')
   })
 
-  it('should preserve custom vary header', async () => {
+  it('should preserve custom vary header and append RSC headers in app route handlers', async () => {
     const res = await next.fetch('/normal')
     const varyHeader = res.headers.get('vary')
 
     // Custom header is preserved
     expect(varyHeader).toContain('User-Agent')
     expect(res.headers.get('cache-control')).toBe('s-maxage=3600')
+
+    // Next.js internal headers are appended
+    expect(varyHeader).toContain('RSC')
+    expect(varyHeader).toContain('Next-Router-State-Tree')
+    expect(varyHeader).toContain('Next-Router-Prefetch')
   })
 
-  it('should preserve middleware vary header', async () => {
+  it('should preserve middleware vary header in combination with route handlers', async () => {
     const res = await next.fetch('/normal')
     const varyHeader = res.headers.get('vary')
     const customHeader = res.headers.get('my-custom-header')
@@ -32,5 +37,10 @@ describe('Vary Header Tests', () => {
     // Both middleware and route handler vary headers are preserved
     expect(varyHeader).toContain('my-custom-header')
     expect(varyHeader).toContain('User-Agent')
+
+    // Next.js internal headers are still present
+    expect(varyHeader).toContain('RSC')
+    expect(varyHeader).toContain('Next-Router-State-Tree')
+    expect(varyHeader).toContain('Next-Router-Prefetch')
   })
 })