From 15ce69052f1fe2cfb074897ac8743abd2313b8c4 Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Mon, 3 Feb 2025 12:58:27 -0800 Subject: [PATCH 1/3] add additional x-middleware-set-cookie filtering (#75561) Previously when we removed this from the response we only did so for requests that flowed through middleware and static handlers. We should ensure it's filtered in `sendResponse` as well. The header is only needed internally. --- packages/next/src/server/send-response.ts | 5 +++++ .../app-dir/app-middleware/app/cookies/api/route.js | 11 +++++++++++ 2 files changed, 16 insertions(+) create mode 100644 test/e2e/app-dir/app-middleware/app/cookies/api/route.js diff --git a/packages/next/src/server/send-response.ts b/packages/next/src/server/send-response.ts index 20dd088b788bb..98ca7e42d7d40 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/cookies/api/route.js b/test/e2e/app-dir/app-middleware/app/cookies/api/route.js new file mode 100644 index 0000000000000..598c70f384dac --- /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 +} From 4dfd9582bf8683938675f7ad350a276aebd715fd Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:49:17 -0800 Subject: [PATCH 2/3] add test --- .../app-dir/app-middleware/app-middleware.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 77e7e8bca5e71..b06d95ffa45c2 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') From 76999084d02aacaf676efcd74e75291f04e4aeb5 Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:54:19 -0800 Subject: [PATCH 3/3] remove unnecessary internal middleware header from response (#73482) x-middleware-set-cookie is an internal header used by the middleware handler and doesn't need to be forwarded onto the response. this also adds handling to filter out internal request headers as they aren't intended to be used externally. --------- Co-authored-by: JJ Kasper --- packages/next/src/server/lib/router-server.ts | 6 +++++ .../server/lib/router-utils/resolve-routes.ts | 8 +++++++ .../next/src/server/lib/server-ipc/utils.ts | 23 +++++++++++++++++++ .../app-middleware/app/cookies/page.js | 6 +++++ .../test/index.test.js | 2 ++ .../required-server-files-app.test.ts | 2 ++ .../required-server-files-i18n.test.ts | 2 ++ .../required-server-files-ppr.test.ts | 2 ++ .../required-server-files.test.ts | 2 ++ .../response-cache/index.test.ts | 2 ++ 10 files changed, 55 insertions(+) create mode 100644 test/e2e/app-dir/app-middleware/app/cookies/page.js diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts index 1739d21bc5112..20aab194b2952 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 a57d6abfae6f7..71201e71086da 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 3c09bf476b8d7..09dee95773625 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/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 0000000000000..cdcfe3addce7f --- /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 ca216733c9316..9821c589ea4b9 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 0860282fb7cf1..bd7073378fa48 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 b99348c689905..9341b75bad517 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 e125319491f63..6ab257ba88537 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 2d8c58fdee412..6d2ad6c53d5e4 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 5f962bf374999..003c21afca7cf 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) })