forked from vercel/next.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support overriding request headers in middlewares (vercel#41380)
This PR adds a feature in middleware to add, modify, or delete request headers. This feature is quite useful to pass data from middleware to Serverless/Edge API routes. ### Questions for Reviewers - Should we deny modifying standard request headers like `Transfer-Encoding`? - Should we throw an error if the header is too large? Real-world HTTP servers will accept up to only 8KB - 32KB. ### New APIs Adds a new option `request.headers` to the `MiddlewareResponseInit` parameter in `NextResponse.next()` and `NextResponse.rewrite()`. It's a [`Header`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object holding *all* request headers. Specifically: ```ts interface MiddlewareResponseInit extends ResponseInit { request?: { headers?: Headers } } ``` ### Example ```ts // pages/api/hello.ts export default (req, res) => { const valueFromMiddleware = req.headers['x-hello-from-middleware'] return res.send(valueFromMiddleware) } // middleware.ts import { NextRequest, NextResponse } from 'next/server' export default function middleware(request: NextRequest) { // Clone request headers const headers = new Headers(request.headers); // Add a new request header headers.set('x-hello-from-middleware', 'foo'); // Delete a request header from the client headers.delete('x-from-client'); const resp = NextResponse.next({ // New option `request.headers` which accepts a Headers object // overrides request headers with the specified new ones. request: { headers } }); // You can still set *response* headers to the client, as before. resp.headers.set('x-hello-client', 'bar'); return resp; } ``` ### New middleware headers - `x-middleware-override-headers`: A comma separated list of *all* request header names. Headers not listed will be deleted. - `x-middleware-request-<name>`: A new value for the header `<name>`. ## Related Discussions - vercel#31188 - vercel#39300 ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` ## Feature - [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [x] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
- Loading branch information
Showing
16 changed files
with
459 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: 'next/headers', | ||
path: '/headers', | ||
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() | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { headers } from 'next/headers' | ||
|
||
export default function SSRPage() { | ||
const headersObj = Object.fromEntries(headers()) | ||
return ( | ||
<> | ||
<p id="headers">{JSON.stringify(headersObj)}</p> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export default function Layout({ children }) { | ||
return ( | ||
<html lang="en"> | ||
<head> | ||
<title>app-middleware</title> | ||
</head> | ||
<body>{children}</body> | ||
</html> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
module.exports = { | ||
experimental: { | ||
appDir: true, | ||
legacyBrowsers: false, | ||
browsersListForSwc: true, | ||
}, | ||
} |
11 changes: 11 additions & 0 deletions
11
test/e2e/app-dir/app-middleware/pages/api/dump-headers-edge.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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', | ||
}, | ||
}) | ||
} |
6 changes: 6 additions & 0 deletions
6
test/e2e/app-dir/app-middleware/pages/api/dump-headers-serverless.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export default (req, res) => { | ||
return res | ||
.status(200) | ||
.setHeader('headers-from-serverless', '1') | ||
.json(req.headers) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.vercel |
30 changes: 30 additions & 0 deletions
30
test/e2e/middleware-request-header-overrides/app/middleware.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}, | ||
}) | ||
} |
1 change: 1 addition & 0 deletions
1
test/e2e/middleware-request-header-overrides/app/next.config.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = {} |
11 changes: 11 additions & 0 deletions
11
test/e2e/middleware-request-header-overrides/app/pages/api/dump-headers-edge.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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', | ||
}, | ||
}) | ||
} |
6 changes: 6 additions & 0 deletions
6
test/e2e/middleware-request-header-overrides/app/pages/api/dump-headers-serverless.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export default (req, res) => { | ||
return res | ||
.status(200) | ||
.setHeader('headers-from-serverless', '1') | ||
.json(req.headers) | ||
} |
15 changes: 15 additions & 0 deletions
15
test/e2e/middleware-request-header-overrides/app/pages/ssr-page.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
export default function SSRPage({ headers }) { | ||
return ( | ||
<> | ||
<p id="headers">{JSON.stringify(headers)}</p> | ||
</> | ||
) | ||
} | ||
|
||
export const getServerSideProps = (ctx) => { | ||
return { | ||
props: { | ||
headers: ctx.req.headers, | ||
}, | ||
} | ||
} |
Oops, something went wrong.