diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 96ef3408a17f6..878e5734f84c8 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -63,6 +63,7 @@ import { } from './spr-cache' import { execOnce } from '../lib/utils' import { isBlockedPage } from './utils' +import { compile as compilePathToRegex } from 'next/dist/compiled/path-to-regexp' import { loadEnvConfig } from '../../lib/load-env-config' const getCustomRouteMatcher = pathMatch(true) @@ -483,9 +484,18 @@ export default class Server { match: route.match, type: route.type, name: `${route.type} ${route.source} header route`, - fn: async (_req, res, _params, _parsedUrl) => { + fn: async (_req, res, params, _parsedUrl) => { for (const header of (route as Header).headers) { - res.setHeader(header.key, header.value) + let { key, value } = header + if (key.includes(':')) { + // see `prepareDestination` util for explanation for + // `validate: false` being used + key = compilePathToRegex(key, { validate: false })(params) + } + if (value.includes(':')) { + value = compilePathToRegex(value, { validate: false })(params) + } + res.setHeader(key, value) } return { finished: false } }, diff --git a/test/integration/custom-routes/next.config.js b/test/integration/custom-routes/next.config.js index ae854925d8ddd..eaba429484a21 100644 --- a/test/integration/custom-routes/next.config.js +++ b/test/integration/custom-routes/next.config.js @@ -217,6 +217,19 @@ module.exports = { }, ], }, + { + source: '/my-other-header/:path', + headers: [ + { + key: 'x-path', + value: ':path', + }, + { + key: 'some:path', + value: 'hi', + }, + ], + }, { source: '/:path*', headers: [ @@ -226,6 +239,19 @@ module.exports = { }, ], }, + { + source: '/named-pattern/:path(.*)', + headers: [ + { + key: 'x-something', + value: 'value=:path', + }, + { + key: 'path-:path', + value: 'end', + }, + ], + }, ] }, }, diff --git a/test/integration/custom-routes/test/index.test.js b/test/integration/custom-routes/test/index.test.js index c3189ffd10dc4..0e0f920dafbed 100644 --- a/test/integration/custom-routes/test/index.test.js +++ b/test/integration/custom-routes/test/index.test.js @@ -306,6 +306,18 @@ const runTests = (isDev = false) => { expect(res.headers.get('x-second-header')).toBe('second') }) + it('should apply params for header key/values', async () => { + const res = await fetchViaHTTP(appPort, '/my-other-header/first') + expect(res.headers.get('x-path')).toBe('first') + expect(res.headers.get('somefirst')).toBe('hi') + }) + + it('should support named pattern for header key/values', async () => { + const res = await fetchViaHTTP(appPort, '/named-pattern/hello') + expect(res.headers.get('x-something')).toBe('value=hello') + expect(res.headers.get('path-hello')).toBe('end') + }) + it('should support proxying to external resource', async () => { const res = await fetchViaHTTP(appPort, '/proxy-me/first?keep=me&and=me') expect(res.status).toBe(200) @@ -601,6 +613,20 @@ const runTests = (isDev = false) => { regex: normalizeRegEx('^\\/my-headers(?:\\/(.*))$'), source: '/my-headers/(.*)', }, + { + headers: [ + { + key: 'x-path', + value: ':path', + }, + { + key: 'some:path', + value: 'hi', + }, + ], + regex: normalizeRegEx('^\\/my-other-header(?:\\/([^\\/]+?))$'), + source: '/my-other-header/:path', + }, { headers: [ { @@ -613,6 +639,20 @@ const runTests = (isDev = false) => { ), source: '/:path*', }, + { + headers: [ + { + key: 'x-something', + value: 'value=:path', + }, + { + key: 'path-:path', + value: 'end', + }, + ], + regex: normalizeRegEx('^\\/named-pattern(?:\\/(.*))$'), + source: '/named-pattern/:path(.*)', + }, ], rewrites: [ {