Chaining or Combining Next.js App Router Middleware #73116
Replies: 3 comments 1 reply
-
Hi @58bits Thanks, but you should come here with question next time. |
Beta Was this translation helpful? Give feedback.
-
Hi, you are setting request headers via request.headers.set function, and it works fine, and u can receive it later in server components, for example, if u need to set nonce for a script |
Beta Was this translation helpful? Give feedback.
-
Here is my version of a middleware import { ResponseCookies } from 'next/dist/compiled/@edge-runtime/cookies'
import { NextMiddleware } from 'next/dist/server/web/types'
import { NextRequest, NextResponse } from 'next/server'
// The prefix Next.js uses for all kinds of middleware headers
export const middlewarePrefix = 'x-middleware-'
// The prefix Next.js uses to store changed request headers as response headers
export const requestHeaderPrefix = 'x-middleware-request-'
// The header Next.js uses to mark a response as a rewrite
export const rewriteHeader = `x-middleware-rewrite`
export const compose = (...middlewares: NextMiddleware[]): NextMiddleware => {
const composedMiddleware: NextMiddleware = async (request, event) => {
const requestHeaders = new Headers(request.headers)
const responseHeaders = new Headers()
const responseCookies = new ResponseCookies(request.headers)
for (const middleware of middlewares) {
// Create a new request with the preserved request headers
// otherwise we might reset updated headers to values from the original request
const newRequest = new NextRequest(request, { ...request, headers: requestHeaders })
// Run the middleware
const middlewareResponse = await middleware(newRequest, event)
// Nothing to do if middleware does not return a response
if (!middlewareResponse) continue
// Stop processing middlewares if response is a redirect
if (middlewareResponse.status >= 300 && middlewareResponse.status < 400)
return middlewareResponse
// Stop processing middlewares if response is a rewrite
if (middlewareResponse.headers.get(rewriteHeader)) return middlewareResponse
// Preserve headers
for (const [key, value] of middlewareResponse.headers.entries()) {
if (key.startsWith(requestHeaderPrefix)) {
requestHeaders.set(key.slice(requestHeaderPrefix.length), value)
} else {
responseHeaders.set(key, value)
}
}
// Preserve cookies
const cookies = new ResponseCookies(middlewareResponse.headers)
for (const cookie of cookies.getAll()) {
responseCookies.set(cookie.name, cookie.value, cookie)
}
}
// Create a new response with the preserved request headers
const response = NextResponse.next({ request: { headers: requestHeaders } })
// Set the preserved response headers and cookies
for (const [key, value] of responseHeaders.entries()) {
if (!key.startsWith(middlewarePrefix)) {
response.headers.set(key, value)
}
}
for (const cookie of responseCookies.getAll()) {
response.cookies.set(cookie.name, cookie.value, cookie)
}
return response
}
return composedMiddleware
} and tests import { ResponseCookies } from 'next/dist/compiled/@edge-runtime/cookies'
import { NextRequest, NextResponse } from 'next/server'
import { beforeEach, expect, it } from 'vitest'
import { compose, requestHeaderPrefix, rewriteHeader } from './compose'
let mockRequest: NextRequest
const mockFetchEvent = {} as any
const noop = () => {}
beforeEach(() => {
const reqHeaders = new Headers()
reqHeaders.set('x-incoming-request-header1', 'initial-value')
reqHeaders.set('x-incoming-request-header2', 'initial-value')
const reqCookies = new ResponseCookies(reqHeaders)
reqCookies.set('x-incoming-cookie1', 'initial-value')
reqCookies.set('x-incoming-cookie2', 'initial-value')
reqHeaders.set('cookie', reqCookies.toString())
mockRequest = new NextRequest('https://example.com/test', { headers: reqHeaders })
})
it('preserves request headers, response headers and cookies', async () => {
const result = (await compose(
request => {
const headers = new Headers(request.headers)
headers.set('x-incoming-request-header2', 'override-value')
headers.set('x-added-request-header1', 'initial-value')
const response = NextResponse.next({ request: { headers } })
response.headers.set('x-response-header1', 'initial-value')
response.cookies.set('x-added-cookie1', 'initial-value')
response.cookies.set('x-incoming-cookie2', 'override-value')
return response
},
() => {
const response = NextResponse.next()
response.cookies.set('x-added-cookie2', 'initial-value')
return response
},
noop,
request => {
const headers = new Headers(request.headers)
headers.set('x-added-request-header2', 'initial-value')
const response = NextResponse.next({ request: { headers } })
response.headers.set('x-response-header2', 'initial-value')
response.cookies.set('x-added-cookie3', 'initial-value')
return response
},
)(mockRequest, mockFetchEvent)) as NextResponse
expect(result.headers.get(requestHeaderPrefix + 'x-incoming-request-header1')).toBe(
'initial-value',
)
expect(result.headers.get(requestHeaderPrefix + 'x-incoming-request-header2')).toBe(
'override-value',
)
expect(result.headers.get(requestHeaderPrefix + 'x-added-request-header1')).toBe('initial-value')
expect(result.headers.get(requestHeaderPrefix + 'x-added-request-header2')).toBe('initial-value')
expect(result.headers.get('x-response-header1')).toBe('initial-value')
expect(result.headers.get('x-response-header2')).toBe('initial-value')
const cookies = new ResponseCookies(result.headers)
expect(cookies.get('x-incoming-cookie1')?.value).toBe('initial-value')
expect(cookies.get('x-incoming-cookie2')?.value).toBe('override-value')
expect(cookies.get('x-added-cookie1')?.value).toBe('initial-value')
expect(cookies.get('x-added-cookie2')?.value).toBe('initial-value')
expect(cookies.get('x-added-cookie3')?.value).toBe('initial-value')
})
it('stops processing middlewares if response is a redirect', async () => {
const result = (await compose(
() => NextResponse.redirect('https://example.com/redirect'),
() => {
throw new Error('This middleware should not be called')
},
)(mockRequest, mockFetchEvent)) as NextResponse
expect(result.status).toBe(307)
expect(result.headers.get('location')).toBe('https://example.com/redirect')
})
it('stops processing middlewares if response is a rewrite', async () => {
const result = (await compose(
() => NextResponse.rewrite('https://example.com/rewrite'),
() => {
throw new Error('This middleware should not be called')
},
)(mockRequest, mockFetchEvent)) as NextResponse
expect(result.status).toBe(200)
expect(result.headers.get(rewriteHeader)).toBe('https://example.com/rewrite')
}) Still think it's quite the head scratcher why the middleware is such a bare bones feature. Would love to see something more like Hono style middleware handling rather than implementing our own route matching and middleware composition. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Hi all - we've published (and updated) our middleware chaining strategy...
https://www.58bits.com/blog/chaining-or-combining-nextjs-middleware
Our
chainMiddleware.ts
helper function now looks like this...Our
types.ts
definition looks like this...Here are a couple of example plugins (a larger collection including withNonce, withAuth, withi18n, are in the blog post)...
withPrefersColorScheme.ts
withCSP.ts
...etc.
Which can all be used in
middleware.ts
like this..Hope this helps....
Beta Was this translation helpful? Give feedback.
All reactions