diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts index 1739d21bc51121..20aab194b29523 100644 --- a/packages/next/src/server/lib/router-server.ts +++ b/packages/next/src/server/lib/router-server.ts @@ -41,6 +41,7 @@ import { getNextPathnameInfo } from '../../shared/lib/router/utils/get-next-path import { getHostname } from '../../shared/lib/get-hostname' import { detectDomainLocale } from '../../shared/lib/i18n/detect-domain-locale' import { normalizedAssetPrefix } from '../../shared/lib/normalized-asset-prefix' +import { filterInternalHeaders } from './server-ipc/utils' const debug = setupDebug('next:router-server:main') const isNextFont = (pathname: string | null) => @@ -149,6 +150,11 @@ export async function initialize(opts: { require('./render-server') as typeof import('./render-server') const requestHandlerImpl: WorkerRequestHandler = async (req, res) => { + // internal headers should not be honored by the request handler + if (!process.env.NEXT_PRIVATE_TEST_HEADERS) { + filterInternalHeaders(req.headers) + } + if ( !opts.minimalMode && config.i18n && diff --git a/packages/next/src/server/lib/router-utils/resolve-routes.ts b/packages/next/src/server/lib/router-utils/resolve-routes.ts index a57d6abfae6f7c..71201e71086dab 100644 --- a/packages/next/src/server/lib/router-utils/resolve-routes.ts +++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts @@ -589,6 +589,14 @@ export function getResolveRoutes( ) { continue } + + // for set-cookie, the header shouldn't be added to the response + // as it's only needed for the request to the middleware function. + if (key === 'x-middleware-set-cookie') { + req.headers[key] = value + continue + } + if (value) { resHeaders[key] = value req.headers[key] = value diff --git a/packages/next/src/server/lib/server-ipc/utils.ts b/packages/next/src/server/lib/server-ipc/utils.ts index 3c09bf476b8d7f..09dee95773625d 100644 --- a/packages/next/src/server/lib/server-ipc/utils.ts +++ b/packages/next/src/server/lib/server-ipc/utils.ts @@ -36,3 +36,26 @@ export const filterReqHeaders = ( } return headers as Record } + +// These are headers that are only used internally and should +// not be honored from the external request +const INTERNAL_HEADERS = [ + 'x-middleware-rewrite', + 'x-middleware-redirect', + 'x-middleware-set-cookie', + 'x-middleware-skip', + 'x-middleware-override-headers', + 'x-middleware-next', + 'x-now-route-matches', + 'x-matched-path', +] + +export const filterInternalHeaders = ( + headers: Record +) => { + for (const header in headers) { + if (INTERNAL_HEADERS.includes(header)) { + delete headers[header] + } + } +} diff --git a/packages/next/src/server/send-response.ts b/packages/next/src/server/send-response.ts index 20dd088b788bb8..98ca7e42d7d405 100644 --- a/packages/next/src/server/send-response.ts +++ b/packages/next/src/server/send-response.ts @@ -25,6 +25,11 @@ export async function sendResponse( // Copy over the response headers. response.headers?.forEach((value, name) => { + // `x-middleware-set-cookie` is an internal header not needed for the response + if (name.toLowerCase() === 'x-middleware-set-cookie') { + return + } + // The append handling is special cased for `set-cookie`. if (name.toLowerCase() === 'set-cookie') { // TODO: (wyattjoh) replace with native response iteration when we can upgrade undici diff --git a/test/e2e/app-dir/app-middleware/app-middleware.test.ts b/test/e2e/app-dir/app-middleware/app-middleware.test.ts index 77e7e8bca5e719..b06d95ffa45c23 100644 --- a/test/e2e/app-dir/app-middleware/app-middleware.test.ts +++ b/test/e2e/app-dir/app-middleware/app-middleware.test.ts @@ -174,6 +174,18 @@ createNextDescribe( await browser.deleteCookies() }) + it('should omit internal headers for middleware cookies', async () => { + const response = await next.fetch('/rsc-cookies/cookie-options') + expect(response.status).toBe(200) + expect(response.headers.get('x-middleware-set-cookie')).toBeNull() + + const response2 = await next.fetch('/cookies/api') + expect(response2.status).toBe(200) + expect(response2.headers.get('x-middleware-set-cookie')).toBeNull() + expect(response2.headers.get('set-cookie')).toBeDefined() + expect(response2.headers.get('set-cookie')).toContain('example') + }) + it('should respect cookie options of merged middleware cookies', async () => { const browser = await next.browser('/rsc-cookies/cookie-options') diff --git a/test/e2e/app-dir/app-middleware/app/cookies/api/route.js b/test/e2e/app-dir/app-middleware/app/cookies/api/route.js new file mode 100644 index 00000000000000..598c70f384dac0 --- /dev/null +++ b/test/e2e/app-dir/app-middleware/app/cookies/api/route.js @@ -0,0 +1,11 @@ +import { NextResponse } from 'next/server' + +export function GET() { + const response = new NextResponse() + response.cookies.set({ + name: 'example', + value: 'example', + }) + + return response +} diff --git a/test/e2e/app-dir/app-middleware/app/cookies/page.js b/test/e2e/app-dir/app-middleware/app/cookies/page.js new file mode 100644 index 00000000000000..cdcfe3addce7f2 --- /dev/null +++ b/test/e2e/app-dir/app-middleware/app/cookies/page.js @@ -0,0 +1,6 @@ +import { cookies } from 'next/headers' + +export default async function Page() { + const cookieLength = (await cookies()).size + return
cookies: {cookieLength}
+} diff --git a/test/integration/required-server-files-ssr-404/test/index.test.js b/test/integration/required-server-files-ssr-404/test/index.test.js index ca216733c9316e..9821c589ea4b90 100644 --- a/test/integration/required-server-files-ssr-404/test/index.test.js +++ b/test/integration/required-server-files-ssr-404/test/index.test.js @@ -44,6 +44,7 @@ describe('Required Server Files', () => { } await fs.rename(join(appDir, 'pages'), join(appDir, 'pages-bak')) + process.env.NEXT_PRIVATE_TEST_HEADERS = '1' nextApp = nextServer({ conf: {}, dir: appDir, @@ -57,6 +58,7 @@ describe('Required Server Files', () => { console.log(`Listening at ::${appPort}`) }) afterAll(async () => { + delete process.env.NEXT_PRIVATE_TEST_HEADERS if (server) server.close() await fs.rename(join(appDir, 'pages-bak'), join(appDir, 'pages')) }) diff --git a/test/production/standalone-mode/required-server-files/required-server-files-app.test.ts b/test/production/standalone-mode/required-server-files/required-server-files-app.test.ts index 0860282fb7cf1e..bd7073378fa483 100644 --- a/test/production/standalone-mode/required-server-files/required-server-files-app.test.ts +++ b/test/production/standalone-mode/required-server-files/required-server-files-app.test.ts @@ -25,6 +25,7 @@ describe('required server files app router', () => { }) => { // test build against environment with next support process.env.NOW_BUILDER = nextEnv ? '1' : '' + process.env.NEXT_PRIVATE_TEST_HEADERS = '1' next = await createNext({ files: { @@ -96,6 +97,7 @@ describe('required server files app router', () => { await setupNext({ nextEnv: true, minimalMode: true }) }) afterAll(async () => { + delete process.env.NEXT_PRIVATE_TEST_HEADERS await next.destroy() if (server) await killApp(server) }) diff --git a/test/production/standalone-mode/required-server-files/required-server-files-i18n.test.ts b/test/production/standalone-mode/required-server-files/required-server-files-i18n.test.ts index b99348c6899053..9341b75bad517e 100644 --- a/test/production/standalone-mode/required-server-files/required-server-files-i18n.test.ts +++ b/test/production/standalone-mode/required-server-files/required-server-files-i18n.test.ts @@ -24,6 +24,7 @@ describe('required server files i18n', () => { beforeAll(async () => { let wasmPkgIsAvailable = false + process.env.NEXT_PRIVATE_TEST_HEADERS = '1' const res = await nodeFetch( `https://registry.npmjs.com/@next/swc-wasm-nodejs/-/swc-wasm-nodejs-${ @@ -128,6 +129,7 @@ describe('required server files i18n', () => { ) }) afterAll(async () => { + delete process.env.NEXT_PRIVATE_TEST_HEADERS await next.destroy() if (server) await killApp(server) }) diff --git a/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts b/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts index e125319491f63c..6ab257ba885378 100644 --- a/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts +++ b/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts @@ -27,6 +27,7 @@ describe('required server files app router', () => { }) => { // test build against environment with next support process.env.NOW_BUILDER = nextEnv ? '1' : '' + process.env.NEXT_PRIVATE_TEST_HEADERS = '1' next = await createNext({ files: { @@ -106,6 +107,7 @@ describe('required server files app router', () => { await setupNext({ nextEnv: true, minimalMode: true }) }) afterAll(async () => { + delete process.env.NEXT_PRIVATE_TEST_HEADERS await next.destroy() if (server) await killApp(server) }) diff --git a/test/production/standalone-mode/required-server-files/required-server-files.test.ts b/test/production/standalone-mode/required-server-files/required-server-files.test.ts index 2d8c58fdee4126..6d2ad6c53d5e4f 100644 --- a/test/production/standalone-mode/required-server-files/required-server-files.test.ts +++ b/test/production/standalone-mode/required-server-files/required-server-files.test.ts @@ -32,6 +32,7 @@ describe('required server files', () => { }) => { // test build against environment with next support process.env.NOW_BUILDER = nextEnv ? '1' : '' + process.env.NEXT_PRIVATE_TEST_HEADERS = '1' next = await createNext({ files: { @@ -139,6 +140,7 @@ describe('required server files', () => { await setupNext({ nextEnv: true, minimalMode: true }) }) afterAll(async () => { + delete process.env.NEXT_PRIVATE_TEST_HEADERS await next.destroy() if (server) await killApp(server) }) diff --git a/test/production/standalone-mode/response-cache/index.test.ts b/test/production/standalone-mode/response-cache/index.test.ts index 5f962bf3749996..003c21afca7cff 100644 --- a/test/production/standalone-mode/response-cache/index.test.ts +++ b/test/production/standalone-mode/response-cache/index.test.ts @@ -22,6 +22,7 @@ describe('minimal-mode-response-cache', () => { beforeAll(async () => { // test build against environment with next support process.env.NOW_BUILDER = '1' + process.env.NEXT_PRIVATE_TEST_HEADERS = '1' next = await createNext({ files: new FileRef(join(__dirname, 'app')), @@ -84,6 +85,7 @@ describe('minimal-mode-response-cache', () => { appPort = `http://127.0.0.1:${port}` }) afterAll(async () => { + delete process.env.NEXT_PRIVATE_TEST_HEADERS await next.destroy() if (server) await killApp(server) })