From 3db1a041569c6d02055dc989cd61ba6aeff68895 Mon Sep 17 00:00:00 2001 From: Seiya Nuta Date: Mon, 3 Oct 2022 10:17:48 +0900 Subject: [PATCH 01/11] Support overriding request headers in middlewares --- packages/next/server/next-server.ts | 28 +++++++ .../server/web/spec-extension/response.ts | 44 +++++++++- .../app/.gitignore | 1 + .../app/middleware.js | 30 +++++++ .../app/next.config.js | 27 ++++++ .../app/pages/api/dump-headers-edge.js | 11 +++ .../app/pages/api/dump-headers-serverless.js | 6 ++ .../app/pages/ssr-page.js | 11 +++ .../test/index.test.ts | 82 +++++++++++++++++++ 9 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 test/e2e/middleware-request-header-manipulation/app/.gitignore create mode 100644 test/e2e/middleware-request-header-manipulation/app/middleware.js create mode 100644 test/e2e/middleware-request-header-manipulation/app/next.config.js create mode 100644 test/e2e/middleware-request-header-manipulation/app/pages/api/dump-headers-edge.js create mode 100644 test/e2e/middleware-request-header-manipulation/app/pages/api/dump-headers-serverless.js create mode 100644 test/e2e/middleware-request-header-manipulation/app/pages/ssr-page.js create mode 100644 test/e2e/middleware-request-header-manipulation/test/index.test.ts diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 06ccb3d4cd553..e4eed33a894b2 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -1873,6 +1873,34 @@ export default class NextNodeServer extends BaseServer { result.response.headers.set('x-middleware-rewrite', rel) } + if (result.response.headers.has('x-middleware-override-headers')) { + const overriddenHeaders: Set = new Set() + for (const key of result.response.headers + .get('x-middleware-override-headers')! + .split(',')) { + overriddenHeaders.add(key.trim()) + } + + // Delete headers. + for (const key of Object.keys(req.headers)) { + if (!overriddenHeaders.has(key)) { + req.headers[key] = undefined + } + } + + // Update or add headers. + for (const key of overriddenHeaders.keys()) { + const oldValue = req.headers[key] + const newValue = result.response.headers.get( + 'x-middleware-request-' + key + ) + + if (oldValue !== newValue) { + req.headers[key] = newValue === null ? undefined : newValue + } + } + } + if (result.response.headers.has('Location')) { const value = result.response.headers.get('Location')! const rel = relativizeURL(value, initUrl) diff --git a/packages/next/server/web/spec-extension/response.ts b/packages/next/server/web/spec-extension/response.ts index 2e19ff0f60c1c..58f1088d18f50 100644 --- a/packages/next/server/web/spec-extension/response.ts +++ b/packages/next/server/web/spec-extension/response.ts @@ -71,19 +71,45 @@ export class NextResponse extends Response { }) } - static rewrite(destination: string | NextURL | URL, init?: ResponseInit) { + static rewrite( + destination: string | NextURL | URL, + init?: MiddlewareResponseInit + ) { const headers = new Headers(init?.headers) headers.set('x-middleware-rewrite', validateURL(destination)) + + handleMiddlewareField(init, headers) return new NextResponse(null, { ...init, headers }) } - static next(init?: ResponseInit) { + static next(init?: MiddlewareResponseInit) { const headers = new Headers(init?.headers) headers.set('x-middleware-next', '1') + + handleMiddlewareField(init, headers) return new NextResponse(null, { ...init, headers }) } } +function handleMiddlewareField( + init: MiddlewareResponseInit | undefined, + headers: Headers +) { + if (init?.request?.headers) { + if (!(init.request.headers instanceof Headers)) { + throw new Error('request.headers must be an instance of Headers') + } + + const keys = [] + for (const [key, value] of init.request.headers) { + headers.set('x-middleware-request-' + key, value) + keys.push(key) + } + + headers.set('x-middleware-override-headers', keys.join(',')) + } +} + interface ResponseInit extends globalThis.ResponseInit { nextConfig?: { basePath?: string @@ -92,3 +118,17 @@ interface ResponseInit extends globalThis.ResponseInit { } url?: string } + +interface ModifiedRequest { + /** + * If this is set, the request headers will be overridden with this value. + */ + headers?: Headers +} + +interface MiddlewareResponseInit extends globalThis.ResponseInit { + /** + * These fields will override the request from clients. + */ + request?: ModifiedRequest +} diff --git a/test/e2e/middleware-request-header-manipulation/app/.gitignore b/test/e2e/middleware-request-header-manipulation/app/.gitignore new file mode 100644 index 0000000000000..e985853ed84ac --- /dev/null +++ b/test/e2e/middleware-request-header-manipulation/app/.gitignore @@ -0,0 +1 @@ +.vercel diff --git a/test/e2e/middleware-request-header-manipulation/app/middleware.js b/test/e2e/middleware-request-header-manipulation/app/middleware.js new file mode 100644 index 0000000000000..ecdedaddff9bd --- /dev/null +++ b/test/e2e/middleware-request-header-manipulation/app/middleware.js @@ -0,0 +1,30 @@ +import { NextResponse, NextCookies } from 'next/server' + +/** + * @param {import('next/server').NextRequest} request + */ +export async function middleware(request) { + const headers = new Headers(request.headers) + headers.set('x-from-middleware', 'hello-from-middleware') + + const removeHeaders = request.nextUrl.searchParams.get('remove-headers') + if (removeHeaders) { + for (const key of removeHeaders.split(',')) { + headers.delete(key) + } + } + + const updateHeader = request.nextUrl.searchParams.get('update-headers') + if (updateHeader) { + for (const kv of updateHeader.split(',')) { + const [key, value] = kv.split('=') + headers.set(key, value) + } + } + + return NextResponse.next({ + request: { + headers, + }, + }) +} diff --git a/test/e2e/middleware-request-header-manipulation/app/next.config.js b/test/e2e/middleware-request-header-manipulation/app/next.config.js new file mode 100644 index 0000000000000..32c5d38016b00 --- /dev/null +++ b/test/e2e/middleware-request-header-manipulation/app/next.config.js @@ -0,0 +1,27 @@ +module.exports = { + i18n: { + locales: ['ja', 'en', 'fr', 'es'], + defaultLocale: 'en', + }, + rewrites() { + return { + beforeFiles: [ + { + source: '/beforefiles-rewrite', + destination: '/ab-test/a', + }, + ], + afterFiles: [ + { + source: '/afterfiles-rewrite', + destination: '/ab-test/b', + }, + { + source: '/afterfiles-rewrite-ssg', + destination: '/fallback-true-blog/first', + }, + ], + fallback: [], + } + }, +} diff --git a/test/e2e/middleware-request-header-manipulation/app/pages/api/dump-headers-edge.js b/test/e2e/middleware-request-header-manipulation/app/pages/api/dump-headers-edge.js new file mode 100644 index 0000000000000..0ece8ea2c7518 --- /dev/null +++ b/test/e2e/middleware-request-header-manipulation/app/pages/api/dump-headers-edge.js @@ -0,0 +1,11 @@ +export const config = { + runtime: 'experimental-edge', +} + +export default (req) => { + return Response.json(Object.fromEntries(req.headers.entries()), { + headers: { + 'headers-from-edge-function': '1', + }, + }) +} diff --git a/test/e2e/middleware-request-header-manipulation/app/pages/api/dump-headers-serverless.js b/test/e2e/middleware-request-header-manipulation/app/pages/api/dump-headers-serverless.js new file mode 100644 index 0000000000000..0f1a9262d9cd0 --- /dev/null +++ b/test/e2e/middleware-request-header-manipulation/app/pages/api/dump-headers-serverless.js @@ -0,0 +1,6 @@ +export default (req, res) => { + return res + .status(200) + .setHeader('headers-from-serverless', '1') + .json(req.headers) +} diff --git a/test/e2e/middleware-request-header-manipulation/app/pages/ssr-page.js b/test/e2e/middleware-request-header-manipulation/app/pages/ssr-page.js new file mode 100644 index 0000000000000..e2aaaa56b46ef --- /dev/null +++ b/test/e2e/middleware-request-header-manipulation/app/pages/ssr-page.js @@ -0,0 +1,11 @@ +export default function SSRPage(props) { + return

{props.message}

+} + +export const getServerSideProps = (req) => { + return { + props: { + message: 'Hello World', + }, + } +} diff --git a/test/e2e/middleware-request-header-manipulation/test/index.test.ts b/test/e2e/middleware-request-header-manipulation/test/index.test.ts new file mode 100644 index 0000000000000..63c374087761a --- /dev/null +++ b/test/e2e/middleware-request-header-manipulation/test/index.test.ts @@ -0,0 +1,82 @@ +/* eslint-env jest */ + +import { join } from 'path' +import { NextInstance } from 'test/lib/next-modes/base' +import { fetchViaHTTP } from 'next-test-utils' +import { createNext, FileRef } from 'e2e-utils' + +describe.each([ + { title: 'Serverless Functions', apiPath: '/api/dump-headers-serverless' }, + { title: 'Edge Functions', apiPath: '/api/dump-headers-edge' }, +])('Middleware Request Headers Manipulation (for $title)', ({ apiPath }) => { + let next: NextInstance + + afterAll(() => next.destroy()) + beforeAll(async () => { + next = await createNext({ + files: { + pages: new FileRef(join(__dirname, '../app/pages')), + 'next.config.js': new FileRef(join(__dirname, '../app/next.config.js')), + 'middleware.js': new FileRef(join(__dirname, '../app/middleware.js')), + }, + }) + }) + + it(`Adds new headers`, async () => { + const res = await fetchViaHTTP(next.url, apiPath, null, { + headers: { + 'x-from-client': 'hello-from-client', + }, + }) + expect(await res.json()).toMatchObject({ + 'x-from-client': 'hello-from-client', + 'x-from-middleware': 'hello-from-middleware', + }) + }) + + it(`Deletes headers`, async () => { + const res = await fetchViaHTTP( + next.url, + apiPath, + { + 'remove-headers': 'x-from-client1,x-from-client2', + }, + { + headers: { + 'x-from-client1': 'hello-from-client', + 'X-From-Client2': 'hello-from-client', + }, + } + ) + + const json = await res.json() + expect(json).not.toHaveProperty('x-from-client1') + expect(json).not.toHaveProperty('X-From-Client2') + expect(json).toMatchObject({ + 'x-from-middleware': 'hello-from-middleware', + }) + }) + + it(`Updates headers`, async () => { + const res = await fetchViaHTTP( + next.url, + apiPath, + { + 'update-headers': 'x-from-client1=new-value1,x-from-client2=new-value2', + }, + { + headers: { + 'x-from-client1': 'old-value1', + 'X-From-Client2': 'old-value2', + 'x-from-client3': 'old-value3', + }, + } + ) + expect(await res.json()).toMatchObject({ + 'x-from-client1': 'new-value1', + 'x-from-client2': 'new-value2', + 'x-from-client3': 'old-value3', + 'x-from-middleware': 'hello-from-middleware', + }) + }) +}) From afe4c787689c52c973a9402ac2ef1429a9269e34 Mon Sep 17 00:00:00 2001 From: Seiya Nuta Date: Thu, 13 Oct 2022 15:01:57 +0900 Subject: [PATCH 02/11] Remove internal headers from the response --- packages/next/server/next-server.ts | 9 +- .../app/middleware.js | 2 +- .../test/index.test.ts | 122 ++++++++++-------- 3 files changed, 78 insertions(+), 55 deletions(-) diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index e4eed33a894b2..f1d721eb78f51 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -1881,6 +1881,8 @@ export default class NextNodeServer extends BaseServer { overriddenHeaders.add(key.trim()) } + result.response.headers.delete('x-middleware-override-headers') + // Delete headers. for (const key of Object.keys(req.headers)) { if (!overriddenHeaders.has(key)) { @@ -1890,14 +1892,15 @@ export default class NextNodeServer extends BaseServer { // Update or add headers. for (const key of overriddenHeaders.keys()) { + const valueKey = 'x-middleware-request-' + key + const newValue = result.response.headers.get(valueKey) const oldValue = req.headers[key] - const newValue = result.response.headers.get( - 'x-middleware-request-' + key - ) if (oldValue !== newValue) { req.headers[key] = newValue === null ? undefined : newValue } + + result.response.headers.delete(valueKey) } } diff --git a/test/e2e/middleware-request-header-manipulation/app/middleware.js b/test/e2e/middleware-request-header-manipulation/app/middleware.js index ecdedaddff9bd..4421a4f37f426 100644 --- a/test/e2e/middleware-request-header-manipulation/app/middleware.js +++ b/test/e2e/middleware-request-header-manipulation/app/middleware.js @@ -1,4 +1,4 @@ -import { NextResponse, NextCookies } from 'next/server' +import { NextResponse } from 'next/server' /** * @param {import('next/server').NextRequest} request diff --git a/test/e2e/middleware-request-header-manipulation/test/index.test.ts b/test/e2e/middleware-request-header-manipulation/test/index.test.ts index 63c374087761a..c5fb25c430356 100644 --- a/test/e2e/middleware-request-header-manipulation/test/index.test.ts +++ b/test/e2e/middleware-request-header-manipulation/test/index.test.ts @@ -5,10 +5,7 @@ import { NextInstance } from 'test/lib/next-modes/base' import { fetchViaHTTP } from 'next-test-utils' import { createNext, FileRef } from 'e2e-utils' -describe.each([ - { title: 'Serverless Functions', apiPath: '/api/dump-headers-serverless' }, - { title: 'Edge Functions', apiPath: '/api/dump-headers-edge' }, -])('Middleware Request Headers Manipulation (for $title)', ({ apiPath }) => { +describe('Middleware Request Headers Manipulation', () => { let next: NextInstance afterAll(() => next.destroy()) @@ -22,61 +19,84 @@ describe.each([ }) }) - it(`Adds new headers`, async () => { - const res = await fetchViaHTTP(next.url, apiPath, null, { - headers: { + describe.each([ + { title: 'Serverless Functions', apiPath: '/api/dump-headers-serverless' }, + { title: 'Edge Functions', apiPath: '/api/dump-headers-edge' }, + ])('$title Backend', ({ apiPath }) => { + it(`Adds new headers`, async () => { + const res = await fetchViaHTTP(next.url, apiPath, null, { + headers: { + 'x-from-client': 'hello-from-client', + }, + }) + expect(await res.json()).toMatchObject({ 'x-from-client': 'hello-from-client', - }, + 'x-from-middleware': 'hello-from-middleware', + }) }) - expect(await res.json()).toMatchObject({ - 'x-from-client': 'hello-from-client', - 'x-from-middleware': 'hello-from-middleware', - }) - }) - it(`Deletes headers`, async () => { - const res = await fetchViaHTTP( - next.url, - apiPath, - { - 'remove-headers': 'x-from-client1,x-from-client2', - }, - { - headers: { - 'x-from-client1': 'hello-from-client', - 'X-From-Client2': 'hello-from-client', + it(`Deletes headers`, async () => { + const res = await fetchViaHTTP( + next.url, + apiPath, + { + 'remove-headers': 'x-from-client1,x-from-client2', }, - } - ) + { + headers: { + 'x-from-client1': 'hello-from-client', + 'X-From-Client2': 'hello-from-client', + }, + } + ) + + const json = await res.json() + expect(json).not.toHaveProperty('x-from-client1') + expect(json).not.toHaveProperty('X-From-Client2') + expect(json).toMatchObject({ + 'x-from-middleware': 'hello-from-middleware', + }) - const json = await res.json() - expect(json).not.toHaveProperty('x-from-client1') - expect(json).not.toHaveProperty('X-From-Client2') - expect(json).toMatchObject({ - 'x-from-middleware': 'hello-from-middleware', + // Should not be included in response headers. + expect(res.headers.get('x-middleware-override-headers')).toBeNull() + expect( + res.headers.get('x-middleware-request-x-from-middleware') + ).toBeNull() + expect(res.headers.get('x-middleware-request-x-from-client1')).toBeNull() + expect(res.headers.get('x-middleware-request-x-from-client2')).toBeNull() }) - }) - it(`Updates headers`, async () => { - const res = await fetchViaHTTP( - next.url, - apiPath, - { - 'update-headers': 'x-from-client1=new-value1,x-from-client2=new-value2', - }, - { - headers: { - 'x-from-client1': 'old-value1', - 'X-From-Client2': 'old-value2', - 'x-from-client3': 'old-value3', + it(`Updates headers`, async () => { + const res = await fetchViaHTTP( + next.url, + apiPath, + { + 'update-headers': + 'x-from-client1=new-value1,x-from-client2=new-value2', }, - } - ) - expect(await res.json()).toMatchObject({ - 'x-from-client1': 'new-value1', - 'x-from-client2': 'new-value2', - 'x-from-client3': 'old-value3', - 'x-from-middleware': 'hello-from-middleware', + { + headers: { + 'x-from-client1': 'old-value1', + 'X-From-Client2': 'old-value2', + 'x-from-client3': 'old-value3', + }, + } + ) + expect(await res.json()).toMatchObject({ + 'x-from-client1': 'new-value1', + 'x-from-client2': 'new-value2', + 'x-from-client3': 'old-value3', + 'x-from-middleware': 'hello-from-middleware', + }) + + // Should not be included in response headers. + expect(res.headers.get('x-middleware-override-headers')).toBeNull() + expect( + res.headers.get('x-middleware-request-x-from-middleware') + ).toBeNull() + expect(res.headers.get('x-middleware-request-x-from-client1')).toBeNull() + expect(res.headers.get('x-middleware-request-x-from-client2')).toBeNull() + expect(res.headers.get('x-middleware-request-x-from-client3')).toBeNull() }) }) }) From 1306149942dedefe83aa8c12753e250353b727d7 Mon Sep 17 00:00:00 2001 From: Seiya Nuta Date: Thu, 13 Oct 2022 15:23:17 +0900 Subject: [PATCH 03/11] Make linter happy --- .../server/web/spec-extension/response.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/next/server/web/spec-extension/response.ts b/packages/next/server/web/spec-extension/response.ts index 58f1088d18f50..386353d897ba6 100644 --- a/packages/next/server/web/spec-extension/response.ts +++ b/packages/next/server/web/spec-extension/response.ts @@ -7,6 +7,25 @@ import { NextCookies } from './cookies' const INTERNALS = Symbol('internal response') const REDIRECTS = new Set([301, 302, 303, 307, 308]) +function handleMiddlewareField( + init: MiddlewareResponseInit | undefined, + headers: Headers +) { + if (init?.request?.headers) { + if (!(init.request.headers instanceof Headers)) { + throw new Error('request.headers must be an instance of Headers') + } + + const keys = [] + for (const [key, value] of init.request.headers) { + headers.set('x-middleware-request-' + key, value) + keys.push(key) + } + + headers.set('x-middleware-override-headers', keys.join(',')) + } +} + export class NextResponse extends Response { [INTERNALS]: { cookies: NextCookies @@ -91,25 +110,6 @@ export class NextResponse extends Response { } } -function handleMiddlewareField( - init: MiddlewareResponseInit | undefined, - headers: Headers -) { - if (init?.request?.headers) { - if (!(init.request.headers instanceof Headers)) { - throw new Error('request.headers must be an instance of Headers') - } - - const keys = [] - for (const [key, value] of init.request.headers) { - headers.set('x-middleware-request-' + key, value) - keys.push(key) - } - - headers.set('x-middleware-override-headers', keys.join(',')) - } -} - interface ResponseInit extends globalThis.ResponseInit { nextConfig?: { basePath?: string From f0048cecae5d84e67e02c33460262c872c88b060 Mon Sep 17 00:00:00 2001 From: Seiya Nuta Date: Fri, 14 Oct 2022 14:43:44 +0900 Subject: [PATCH 04/11] Add test cases for gSSP --- .../app/pages/ssr-page.js | 12 ++++--- .../test/index.test.ts | 33 ++++++++++++++----- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/test/e2e/middleware-request-header-manipulation/app/pages/ssr-page.js b/test/e2e/middleware-request-header-manipulation/app/pages/ssr-page.js index e2aaaa56b46ef..3ff46de857025 100644 --- a/test/e2e/middleware-request-header-manipulation/app/pages/ssr-page.js +++ b/test/e2e/middleware-request-header-manipulation/app/pages/ssr-page.js @@ -1,11 +1,15 @@ -export default function SSRPage(props) { - return

{props.message}

+export default function SSRPage({ headers }) { + return ( + <> +

{JSON.stringify({ headers })}

+ + ) } -export const getServerSideProps = (req) => { +export const getServerSideProps = (ctx) => { return { props: { - message: 'Hello World', + headers: ctx.req.headers, }, } } diff --git a/test/e2e/middleware-request-header-manipulation/test/index.test.ts b/test/e2e/middleware-request-header-manipulation/test/index.test.ts index c5fb25c430356..0b0900b0173af 100644 --- a/test/e2e/middleware-request-header-manipulation/test/index.test.ts +++ b/test/e2e/middleware-request-header-manipulation/test/index.test.ts @@ -4,6 +4,7 @@ import { join } from 'path' import { NextInstance } from 'test/lib/next-modes/base' import { fetchViaHTTP } from 'next-test-utils' import { createNext, FileRef } from 'e2e-utils' +import cheerio from 'cheerio' describe('Middleware Request Headers Manipulation', () => { let next: NextInstance @@ -20,16 +21,30 @@ describe('Middleware Request Headers Manipulation', () => { }) describe.each([ - { title: 'Serverless Functions', apiPath: '/api/dump-headers-serverless' }, - { title: 'Edge Functions', apiPath: '/api/dump-headers-edge' }, - ])('$title Backend', ({ apiPath }) => { + { + title: 'Serverless Functions', + path: '/api/dump-headers-serverless', + toJson: (res: Response) => res.json(), + }, + { + title: 'Edge Functions', + path: '/api/dump-headers-edge', + toJson: (res: Response) => res.json(), + }, + { + title: 'getServerSideProps', + path: '/api/ssr-page', + toJson: async (res: Response) => + cheerio.load(await res.text())('#headers'), + }, + ])('$title Backend', ({ path, toJson }) => { it(`Adds new headers`, async () => { - const res = await fetchViaHTTP(next.url, apiPath, null, { + const res = await fetchViaHTTP(next.url, path, null, { headers: { 'x-from-client': 'hello-from-client', }, }) - expect(await res.json()).toMatchObject({ + expect(await toJson(res)).toMatchObject({ 'x-from-client': 'hello-from-client', 'x-from-middleware': 'hello-from-middleware', }) @@ -38,7 +53,7 @@ describe('Middleware Request Headers Manipulation', () => { it(`Deletes headers`, async () => { const res = await fetchViaHTTP( next.url, - apiPath, + path, { 'remove-headers': 'x-from-client1,x-from-client2', }, @@ -50,7 +65,7 @@ describe('Middleware Request Headers Manipulation', () => { } ) - const json = await res.json() + const json = await toJson(res) expect(json).not.toHaveProperty('x-from-client1') expect(json).not.toHaveProperty('X-From-Client2') expect(json).toMatchObject({ @@ -69,7 +84,7 @@ describe('Middleware Request Headers Manipulation', () => { it(`Updates headers`, async () => { const res = await fetchViaHTTP( next.url, - apiPath, + path, { 'update-headers': 'x-from-client1=new-value1,x-from-client2=new-value2', @@ -82,7 +97,7 @@ describe('Middleware Request Headers Manipulation', () => { }, } ) - expect(await res.json()).toMatchObject({ + expect(await toJson(res)).toMatchObject({ 'x-from-client1': 'new-value1', 'x-from-client2': 'new-value2', 'x-from-client3': 'old-value3', From 77956ee22bfcde89db3cd2b76bb811499cc6da79 Mon Sep 17 00:00:00 2001 From: Seiya Nuta Date: Fri, 14 Oct 2022 17:30:37 +0900 Subject: [PATCH 05/11] Fix tests for gSSP --- .../app/pages/ssr-page.js | 2 +- .../test/index.test.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/test/e2e/middleware-request-header-manipulation/app/pages/ssr-page.js b/test/e2e/middleware-request-header-manipulation/app/pages/ssr-page.js index 3ff46de857025..ed2e4a6fcce82 100644 --- a/test/e2e/middleware-request-header-manipulation/app/pages/ssr-page.js +++ b/test/e2e/middleware-request-header-manipulation/app/pages/ssr-page.js @@ -1,7 +1,7 @@ export default function SSRPage({ headers }) { return ( <> -

{JSON.stringify({ headers })}

+

{JSON.stringify(headers)}

) } diff --git a/test/e2e/middleware-request-header-manipulation/test/index.test.ts b/test/e2e/middleware-request-header-manipulation/test/index.test.ts index 0b0900b0173af..6a23ee392ba0d 100644 --- a/test/e2e/middleware-request-header-manipulation/test/index.test.ts +++ b/test/e2e/middleware-request-header-manipulation/test/index.test.ts @@ -33,9 +33,11 @@ describe('Middleware Request Headers Manipulation', () => { }, { title: 'getServerSideProps', - path: '/api/ssr-page', - toJson: async (res: Response) => - cheerio.load(await res.text())('#headers'), + path: '/ssr-page', + toJson: async (res: Response) => { + const $ = cheerio.load(await res.text()) + return JSON.parse($('#headers').text()) + }, }, ])('$title Backend', ({ path, toJson }) => { it(`Adds new headers`, async () => { From b955445061da2340c36103f96061bd67bc3b3699 Mon Sep 17 00:00:00 2001 From: Seiya Nuta Date: Fri, 14 Oct 2022 17:40:14 +0900 Subject: [PATCH 06/11] test: Remove unused config --- .../app/next.config.js | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/test/e2e/middleware-request-header-manipulation/app/next.config.js b/test/e2e/middleware-request-header-manipulation/app/next.config.js index 32c5d38016b00..4ba52ba2c8df6 100644 --- a/test/e2e/middleware-request-header-manipulation/app/next.config.js +++ b/test/e2e/middleware-request-header-manipulation/app/next.config.js @@ -1,27 +1 @@ -module.exports = { - i18n: { - locales: ['ja', 'en', 'fr', 'es'], - defaultLocale: 'en', - }, - rewrites() { - return { - beforeFiles: [ - { - source: '/beforefiles-rewrite', - destination: '/ab-test/a', - }, - ], - afterFiles: [ - { - source: '/afterfiles-rewrite', - destination: '/ab-test/b', - }, - { - source: '/afterfiles-rewrite-ssg', - destination: '/fallback-true-blog/first', - }, - ], - fallback: [], - } - }, -} +module.exports = {} From 6430cff902f92d6fd28ca9ea175f765217e6b5f7 Mon Sep 17 00:00:00 2001 From: Seiya Nuta Date: Fri, 14 Oct 2022 17:54:04 +0900 Subject: [PATCH 07/11] Prefer delete over setting undefined --- packages/next/server/next-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index a1a61b5a3817d..e6e782a3adbbf 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -1921,7 +1921,7 @@ export default class NextNodeServer extends BaseServer { // Delete headers. for (const key of Object.keys(req.headers)) { if (!overriddenHeaders.has(key)) { - req.headers[key] = undefined + delete req.headers[key] } } From 664e101cafa6e8cd0c8739251c48557ed627b96e Mon Sep 17 00:00:00 2001 From: Seiya Nuta Date: Fri, 14 Oct 2022 18:40:44 +0900 Subject: [PATCH 08/11] Add a test in app-dir --- test/e2e/app-dir/app-middleware.test.ts | 129 ++++++++++++++++++ test/e2e/app-dir/app-middleware/app/layout.js | 10 ++ .../app-middleware/app/ssr-page.server.js | 15 ++ test/e2e/app-dir/app-middleware/middleware.js | 30 ++++ .../e2e/app-dir/app-middleware/next.config.js | 7 + .../pages/api/dump-headers-edge.js | 11 ++ .../pages/api/dump-headers-serverless.js | 6 + 7 files changed, 208 insertions(+) create mode 100644 test/e2e/app-dir/app-middleware.test.ts create mode 100644 test/e2e/app-dir/app-middleware/app/layout.js create mode 100644 test/e2e/app-dir/app-middleware/app/ssr-page.server.js create mode 100644 test/e2e/app-dir/app-middleware/middleware.js create mode 100644 test/e2e/app-dir/app-middleware/next.config.js create mode 100644 test/e2e/app-dir/app-middleware/pages/api/dump-headers-edge.js create mode 100644 test/e2e/app-dir/app-middleware/pages/api/dump-headers-serverless.js diff --git a/test/e2e/app-dir/app-middleware.test.ts b/test/e2e/app-dir/app-middleware.test.ts new file mode 100644 index 0000000000000..5a78ae06fcd5a --- /dev/null +++ b/test/e2e/app-dir/app-middleware.test.ts @@ -0,0 +1,129 @@ +/* eslint-env jest */ + +import { NextInstance } from 'test/lib/next-modes/base' +import { fetchViaHTTP } from 'next-test-utils' +import { createNext, FileRef } from 'e2e-utils' +import cheerio from 'cheerio' +import path from 'path' + +describe('app-dir with middleware', () => { + if ((global as any).isNextDeploy) { + it('should skip next deploy for now', () => {}) + return + } + + if (process.env.NEXT_TEST_REACT_VERSION === '^17') { + it('should skip for react v17', () => {}) + return + } + + let next: NextInstance + + afterAll(() => next.destroy()) + beforeAll(async () => { + next = await createNext({ + files: new FileRef(path.join(__dirname, 'app-middleware')), + dependencies: { + react: 'experimental', + 'react-dom': 'experimental', + }, + }) + }) + + describe.each([ + { + title: 'Serverless Functions', + path: '/api/dump-headers-serverless', + toJson: (res: Response) => res.json(), + }, + { + title: 'Edge Functions', + path: '/api/dump-headers-edge', + toJson: (res: Response) => res.json(), + }, + { + title: 'getServerSideProps', + path: '/ssr-page', + toJson: async (res: Response) => { + const $ = cheerio.load(await res.text()) + return JSON.parse($('#headers').text()) + }, + }, + ])('Mutate request headers for $title', ({ path, toJson }) => { + it(`Adds new headers`, async () => { + const res = await fetchViaHTTP(next.url, path, null, { + headers: { + 'x-from-client': 'hello-from-client', + }, + }) + expect(await toJson(res)).toMatchObject({ + 'x-from-client': 'hello-from-client', + 'x-from-middleware': 'hello-from-middleware', + }) + }) + + it(`Deletes headers`, async () => { + const res = await fetchViaHTTP( + next.url, + path, + { + 'remove-headers': 'x-from-client1,x-from-client2', + }, + { + headers: { + 'x-from-client1': 'hello-from-client', + 'X-From-Client2': 'hello-from-client', + }, + } + ) + + const json = await toJson(res) + expect(json).not.toHaveProperty('x-from-client1') + expect(json).not.toHaveProperty('X-From-Client2') + expect(json).toMatchObject({ + 'x-from-middleware': 'hello-from-middleware', + }) + + // Should not be included in response headers. + expect(res.headers.get('x-middleware-override-headers')).toBeNull() + expect( + res.headers.get('x-middleware-request-x-from-middleware') + ).toBeNull() + expect(res.headers.get('x-middleware-request-x-from-client1')).toBeNull() + expect(res.headers.get('x-middleware-request-x-from-client2')).toBeNull() + }) + + it(`Updates headers`, async () => { + const res = await fetchViaHTTP( + next.url, + path, + { + 'update-headers': + 'x-from-client1=new-value1,x-from-client2=new-value2', + }, + { + headers: { + 'x-from-client1': 'old-value1', + 'X-From-Client2': 'old-value2', + 'x-from-client3': 'old-value3', + }, + } + ) + expect(await toJson(res)).toMatchObject({ + 'x-from-client1': 'new-value1', + 'x-from-client2': 'new-value2', + 'x-from-client3': 'old-value3', + 'x-from-middleware': 'hello-from-middleware', + }) + + // Should not be included in response headers. + expect(res.headers.get('x-middleware-override-headers')).toBeNull() + expect( + res.headers.get('x-middleware-request-x-from-middleware') + ).toBeNull() + expect(res.headers.get('x-middleware-request-x-from-client1')).toBeNull() + expect(res.headers.get('x-middleware-request-x-from-client2')).toBeNull() + expect(res.headers.get('x-middleware-request-x-from-client3')).toBeNull() + }) + }) +}) diff --git a/test/e2e/app-dir/app-middleware/app/layout.js b/test/e2e/app-dir/app-middleware/app/layout.js new file mode 100644 index 0000000000000..3a1af60bc8b98 --- /dev/null +++ b/test/e2e/app-dir/app-middleware/app/layout.js @@ -0,0 +1,10 @@ +export default function Layout({ children }) { + return ( + + + app-middleware + + {children} + + ) +} diff --git a/test/e2e/app-dir/app-middleware/app/ssr-page.server.js b/test/e2e/app-dir/app-middleware/app/ssr-page.server.js new file mode 100644 index 0000000000000..ed2e4a6fcce82 --- /dev/null +++ b/test/e2e/app-dir/app-middleware/app/ssr-page.server.js @@ -0,0 +1,15 @@ +export default function SSRPage({ headers }) { + return ( + <> +

{JSON.stringify(headers)}

+ + ) +} + +export const getServerSideProps = (ctx) => { + return { + props: { + headers: ctx.req.headers, + }, + } +} diff --git a/test/e2e/app-dir/app-middleware/middleware.js b/test/e2e/app-dir/app-middleware/middleware.js new file mode 100644 index 0000000000000..4421a4f37f426 --- /dev/null +++ b/test/e2e/app-dir/app-middleware/middleware.js @@ -0,0 +1,30 @@ +import { NextResponse } from 'next/server' + +/** + * @param {import('next/server').NextRequest} request + */ +export async function middleware(request) { + const headers = new Headers(request.headers) + headers.set('x-from-middleware', 'hello-from-middleware') + + const removeHeaders = request.nextUrl.searchParams.get('remove-headers') + if (removeHeaders) { + for (const key of removeHeaders.split(',')) { + headers.delete(key) + } + } + + const updateHeader = request.nextUrl.searchParams.get('update-headers') + if (updateHeader) { + for (const kv of updateHeader.split(',')) { + const [key, value] = kv.split('=') + headers.set(key, value) + } + } + + return NextResponse.next({ + request: { + headers, + }, + }) +} diff --git a/test/e2e/app-dir/app-middleware/next.config.js b/test/e2e/app-dir/app-middleware/next.config.js new file mode 100644 index 0000000000000..a928ea943ce24 --- /dev/null +++ b/test/e2e/app-dir/app-middleware/next.config.js @@ -0,0 +1,7 @@ +module.exports = { + experimental: { + appDir: true, + legacyBrowsers: false, + browsersListForSwc: true, + }, +} diff --git a/test/e2e/app-dir/app-middleware/pages/api/dump-headers-edge.js b/test/e2e/app-dir/app-middleware/pages/api/dump-headers-edge.js new file mode 100644 index 0000000000000..0ece8ea2c7518 --- /dev/null +++ b/test/e2e/app-dir/app-middleware/pages/api/dump-headers-edge.js @@ -0,0 +1,11 @@ +export const config = { + runtime: 'experimental-edge', +} + +export default (req) => { + return Response.json(Object.fromEntries(req.headers.entries()), { + headers: { + 'headers-from-edge-function': '1', + }, + }) +} diff --git a/test/e2e/app-dir/app-middleware/pages/api/dump-headers-serverless.js b/test/e2e/app-dir/app-middleware/pages/api/dump-headers-serverless.js new file mode 100644 index 0000000000000..0f1a9262d9cd0 --- /dev/null +++ b/test/e2e/app-dir/app-middleware/pages/api/dump-headers-serverless.js @@ -0,0 +1,6 @@ +export default (req, res) => { + return res + .status(200) + .setHeader('headers-from-serverless', '1') + .json(req.headers) +} From f47e555d8c6f9418f281ad595acb8a1efea7889a Mon Sep 17 00:00:00 2001 From: Seiya Nuta Date: Wed, 19 Oct 2022 09:47:18 +0900 Subject: [PATCH 09/11] test: "header manipulation" -> "header overrides" --- .../app/.gitignore | 0 .../app/middleware.js | 0 .../app/next.config.js | 0 .../app/pages/api/dump-headers-edge.js | 0 .../app/pages/api/dump-headers-serverless.js | 0 .../app/pages/ssr-page.js | 0 .../test/index.test.ts | 2 +- 7 files changed, 1 insertion(+), 1 deletion(-) rename test/e2e/{middleware-request-header-manipulation => middleware-request-header-overrides}/app/.gitignore (100%) rename test/e2e/{middleware-request-header-manipulation => middleware-request-header-overrides}/app/middleware.js (100%) rename test/e2e/{middleware-request-header-manipulation => middleware-request-header-overrides}/app/next.config.js (100%) rename test/e2e/{middleware-request-header-manipulation => middleware-request-header-overrides}/app/pages/api/dump-headers-edge.js (100%) rename test/e2e/{middleware-request-header-manipulation => middleware-request-header-overrides}/app/pages/api/dump-headers-serverless.js (100%) rename test/e2e/{middleware-request-header-manipulation => middleware-request-header-overrides}/app/pages/ssr-page.js (100%) rename test/e2e/{middleware-request-header-manipulation => middleware-request-header-overrides}/test/index.test.ts (98%) diff --git a/test/e2e/middleware-request-header-manipulation/app/.gitignore b/test/e2e/middleware-request-header-overrides/app/.gitignore similarity index 100% rename from test/e2e/middleware-request-header-manipulation/app/.gitignore rename to test/e2e/middleware-request-header-overrides/app/.gitignore diff --git a/test/e2e/middleware-request-header-manipulation/app/middleware.js b/test/e2e/middleware-request-header-overrides/app/middleware.js similarity index 100% rename from test/e2e/middleware-request-header-manipulation/app/middleware.js rename to test/e2e/middleware-request-header-overrides/app/middleware.js diff --git a/test/e2e/middleware-request-header-manipulation/app/next.config.js b/test/e2e/middleware-request-header-overrides/app/next.config.js similarity index 100% rename from test/e2e/middleware-request-header-manipulation/app/next.config.js rename to test/e2e/middleware-request-header-overrides/app/next.config.js diff --git a/test/e2e/middleware-request-header-manipulation/app/pages/api/dump-headers-edge.js b/test/e2e/middleware-request-header-overrides/app/pages/api/dump-headers-edge.js similarity index 100% rename from test/e2e/middleware-request-header-manipulation/app/pages/api/dump-headers-edge.js rename to test/e2e/middleware-request-header-overrides/app/pages/api/dump-headers-edge.js diff --git a/test/e2e/middleware-request-header-manipulation/app/pages/api/dump-headers-serverless.js b/test/e2e/middleware-request-header-overrides/app/pages/api/dump-headers-serverless.js similarity index 100% rename from test/e2e/middleware-request-header-manipulation/app/pages/api/dump-headers-serverless.js rename to test/e2e/middleware-request-header-overrides/app/pages/api/dump-headers-serverless.js diff --git a/test/e2e/middleware-request-header-manipulation/app/pages/ssr-page.js b/test/e2e/middleware-request-header-overrides/app/pages/ssr-page.js similarity index 100% rename from test/e2e/middleware-request-header-manipulation/app/pages/ssr-page.js rename to test/e2e/middleware-request-header-overrides/app/pages/ssr-page.js diff --git a/test/e2e/middleware-request-header-manipulation/test/index.test.ts b/test/e2e/middleware-request-header-overrides/test/index.test.ts similarity index 98% rename from test/e2e/middleware-request-header-manipulation/test/index.test.ts rename to test/e2e/middleware-request-header-overrides/test/index.test.ts index 6a23ee392ba0d..03f7296b5b176 100644 --- a/test/e2e/middleware-request-header-manipulation/test/index.test.ts +++ b/test/e2e/middleware-request-header-overrides/test/index.test.ts @@ -6,7 +6,7 @@ import { fetchViaHTTP } from 'next-test-utils' import { createNext, FileRef } from 'e2e-utils' import cheerio from 'cheerio' -describe('Middleware Request Headers Manipulation', () => { +describe('Middleware Request Headers Overrides', () => { let next: NextInstance afterAll(() => next.destroy()) From 809ba96a729b56d40180077571e61610fa5694db Mon Sep 17 00:00:00 2001 From: Seiya Nuta Date: Wed, 19 Oct 2022 10:09:51 +0900 Subject: [PATCH 10/11] Use next/headers --- test/e2e/app-dir/app-middleware.test.ts | 4 ++-- .../app-dir/app-middleware/app/headers/page.js | 9 +++++++++ .../app-dir/app-middleware/app/ssr-page.server.js | 15 --------------- 3 files changed, 11 insertions(+), 17 deletions(-) create mode 100644 test/e2e/app-dir/app-middleware/app/headers/page.js delete mode 100644 test/e2e/app-dir/app-middleware/app/ssr-page.server.js diff --git a/test/e2e/app-dir/app-middleware.test.ts b/test/e2e/app-dir/app-middleware.test.ts index 5a78ae06fcd5a..9b6c3a9ca3d25 100644 --- a/test/e2e/app-dir/app-middleware.test.ts +++ b/test/e2e/app-dir/app-middleware.test.ts @@ -42,8 +42,8 @@ describe('app-dir with middleware', () => { toJson: (res: Response) => res.json(), }, { - title: 'getServerSideProps', - path: '/ssr-page', + title: '/headers', + path: '/headers', toJson: async (res: Response) => { const $ = cheerio.load(await res.text()) return JSON.parse($('#headers').text()) diff --git a/test/e2e/app-dir/app-middleware/app/headers/page.js b/test/e2e/app-dir/app-middleware/app/headers/page.js new file mode 100644 index 0000000000000..948da4eb69472 --- /dev/null +++ b/test/e2e/app-dir/app-middleware/app/headers/page.js @@ -0,0 +1,9 @@ +import { headers } from 'next/headers' + +export default function SSRPage() { + return ( + <> +

{JSON.stringify(headers())}

+ + ) +} diff --git a/test/e2e/app-dir/app-middleware/app/ssr-page.server.js b/test/e2e/app-dir/app-middleware/app/ssr-page.server.js deleted file mode 100644 index ed2e4a6fcce82..0000000000000 --- a/test/e2e/app-dir/app-middleware/app/ssr-page.server.js +++ /dev/null @@ -1,15 +0,0 @@ -export default function SSRPage({ headers }) { - return ( - <> -

{JSON.stringify(headers)}

- - ) -} - -export const getServerSideProps = (ctx) => { - return { - props: { - headers: ctx.req.headers, - }, - } -} From 598026126670765179fcc85dca9e4791a4ed5279 Mon Sep 17 00:00:00 2001 From: Seiya Nuta Date: Wed, 19 Oct 2022 10:19:25 +0900 Subject: [PATCH 11/11] Fix the test for the next/headers case --- test/e2e/app-dir/app-middleware.test.ts | 2 +- test/e2e/app-dir/app-middleware/app/headers/page.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/e2e/app-dir/app-middleware.test.ts b/test/e2e/app-dir/app-middleware.test.ts index 9b6c3a9ca3d25..b92ffa9352e9e 100644 --- a/test/e2e/app-dir/app-middleware.test.ts +++ b/test/e2e/app-dir/app-middleware.test.ts @@ -42,7 +42,7 @@ describe('app-dir with middleware', () => { toJson: (res: Response) => res.json(), }, { - title: '/headers', + title: 'next/headers', path: '/headers', toJson: async (res: Response) => { const $ = cheerio.load(await res.text()) diff --git a/test/e2e/app-dir/app-middleware/app/headers/page.js b/test/e2e/app-dir/app-middleware/app/headers/page.js index 948da4eb69472..7e9456f738014 100644 --- a/test/e2e/app-dir/app-middleware/app/headers/page.js +++ b/test/e2e/app-dir/app-middleware/app/headers/page.js @@ -1,9 +1,10 @@ import { headers } from 'next/headers' export default function SSRPage() { + const headersObj = Object.fromEntries(headers()) return ( <> -

{JSON.stringify(headers())}

+

{JSON.stringify(headersObj)}

) }