Skip to content

feat: add Next 13 request header mutation to middleware #1866

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions demos/middleware/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ export async function middleware(req: NextRequest) {
let response
const { pathname } = req.nextUrl

if (pathname.startsWith('/api/hello')) {
// Next 13 request header mutation functionality
const headers = new Headers(req.headers)

headers.set('x-hello', 'world')
return NextResponse.next({
request: {
headers
}
})
}

const request = new MiddlewareRequest(req)
if (pathname.startsWith('/static')) {
// Unlike NextResponse.next(), this actually sends the request to the origin
Expand All @@ -24,12 +36,6 @@ export async function middleware(req: NextRequest) {
return res
}

if (pathname.startsWith('/api/hello')) {
// Add a header to the request
req.headers.set('x-hello', 'world')
return request.next()
}

if (pathname.startsWith('/api/geo')) {
req.headers.set('x-geo-country', req.geo.country)
req.headers.set('x-geo-region', req.geo.region)
Expand Down
277 changes: 275 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@
"!**/test/fixtures/**",
"!**/test/sample/**"
],
"moduleNameMapper": {
"https://deno.land/x/html_rewriter@v0.1.0-pre.17": "<rootDir>/test/mockedDenoModules/html_rewriter"
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use deno test instead of Jest for EF stuff

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"transform": {
"\\.[jt]sx?$": "babel-jest"
},
Expand Down
30 changes: 30 additions & 0 deletions packages/runtime/src/templates/edge-shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,34 @@ function isMiddlewareResponse(response: Response | MiddlewareResponse): response
return 'dataTransforms' in response
}

// Next 13 supports request header mutations and has the side effect of prepending header values with 'x-middleware-request'
// as part of invoking NextResponse.next() in the middleware. We need to remove that before sending the response the user
// as the code that removes it in Next isn't run based on how we handle the middleware
//
// Related Next.js code:
// * https://github.com/vercel/next.js/blob/68d06fe015b28d8f81da52ca107a5f4bd72ab37c/packages/next/server/next-server.ts#L1918-L1928
// * https://github.com/vercel/next.js/blob/43c9d8940dc42337dd2f7d66aa90e6abf952278e/packages/next/server/web/spec-extension/response.ts#L10-L27
export function updateModifiedHeaders(response: Response) {
const overriddenHeaders = response.headers.get('x-middleware-override-headers') || ''
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for the fallback if you're checking for falsiness right away


if (!overriddenHeaders) {
return response
}

const headersToUpdate = overriddenHeaders.split(',')

for (const header of headersToUpdate) {
const oldHeaderKey = 'x-middleware-request-' + header
const headerValue = response.headers.get(oldHeaderKey) || ''

response.headers.set(header, headerValue)
Copy link
Contributor

@ascorbic ascorbic Jan 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to set these in the response - it's the request that needs them, so we should be calling this before calling context.next() and setting them on req instead

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request headers appear to be fine in that they don't have the x-middleware-request string prefixed to every header, nor does it contain the x-middleware-override-headers header - it's the result.response headers that have them and are not having them cleaned up.

Would be happy to pair on this tomorrow though as there's likely more insight that you have that I'm missing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not setting them on the request though: https://deploy-preview-1866--next-plugin-edge-middleware.netlify.app/api/hello
They're not meant to be on the response at all, so they need to be stripped and not added back in. They're just meant to be in the request that sent to the origin.

response.headers.delete(oldHeaderKey)
}
response.headers.delete('x-middleware-override-headers')

return response
}

export const buildResponse = async ({
result,
request,
Expand All @@ -68,6 +96,8 @@ export const buildResponse = async ({
request: Request
context: Context
}) => {
result.response = updateModifiedHeaders(result.response)

// They've returned the MiddlewareRequest directly, so we'll call `next()` for them.
if (isMiddlewareRequest(result.response)) {
result.response = await result.response.next()
Expand Down
2 changes: 2 additions & 0 deletions test/mockedDenoModules/html_rewriter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const ElementHandlers = jest.fn()
export const HTMLRewriter = jest.fn()
Loading