Files
next.js/test/e2e/custom-routes/custom-routes.test.ts
Tim NeutkensandGitHub 8141dcf12e Convert test/integration to isolated tests (#93247)
### What?

Converts every test under `test/integration/` to an isolated test
running through `nextTestSetup` (under `test/e2e/`, `test/production/`,
`test/development/`, or `test/unit/`), then deletes `test/integration/`
along with the legacy CI orchestration that was specific to it.

- `test/integration/` removed entirely (~327 test suites)
- New isolated suites added across the existing folders:
  - `test/e2e/` — 175
  - `test/production/` — 130
  - `test/development/` — 43
  - `test/unit/` — 1
- `.github/workflows/build_and_test.yml` and `run-tests.js` no longer
have any `integration` branches
- `nextTestSetup` gained a `baseUrl` option on `next.browser()` so a
small number of tests that drive their own proxy/static-export server
can keep using `next.browser(...)` instead of importing `next-webdriver`
directly

### Why?

`test/integration/` predated `nextTestSetup` and ran tests directly
against the source checkout via custom helpers (`launchApp`,
`nextBuild`, `nextStart`, `runNextCommand`, `webdriver`, `fetchViaHTTP`,
…). Each suite hand-rolled its own dev/start/build orchestration,
fixture mutation, and process management.

The isolated test model used by the rest of the repo gives each suite an
isolated working directory containing a packed `next.tgz` install, a
uniform `next.start()` / `next.build()` / `next.fetch()` /
`next.browser()` API, and the same lifecycle for dev, start, and deploy
modes — so a single set of assertions covers all three. Deploy-mode
skips and per-feature gates are expressed declaratively
(`skipDeployment`, `disableAutoSkewProtection`, `if (skipped) return`)
instead of branching on `process.env`.

Removing `test/integration/` lets us:

- Delete the bespoke orchestration code in the CI workflow and
`run-tests.js`
- Run every converted suite consistently in dev, start, and deploy modes
(where applicable)
- Reproduce every test locally with the same `pnpm
test-{dev,start}-{turbo,webpack}` commands; no separate `integration`
path
- Open the door to running `test/production` against deployments in the
future (the converted suites already declare `skipDeployment` so they
can be flipped on)

### How?

Mechanical conversion per suite, with targeted clean-ups:

1. **Per-suite conversion.** Each
`test/integration/<name>/test/index.test.{js,ts}` was rewritten into a
single `<name>.test.ts` under the right folder based on what the
original exercised:
   - `launchApp` / dev-only assertions → `test/development/`
- `nextBuild` + `nextStart` / start-only assertions → `test/production/`
   - Both → `test/e2e/`
- The one pure jsdom render check (`link-without-router`) → `test/unit/`

2. **API mapping.** Custom helpers were replaced by `nextTestSetup`
equivalents: `launchApp` → `next.start()`, `nextBuild` → `next.build()`,
`runNextCommand` → `next.runCommand`, `fetchViaHTTP` → `next.fetch`,
`webdriver(...)` → `next.browser(...)`. Fixture mutations switched from
raw `fs.writeFile`/`fs.rename` to `next.patchFile` (with the 3-arg
`runWithTempContent` callback when the change has a defined scope) and
`next.deleteFile`.

3. **Deploy-mode handling.** Suites that can't run in deploy mode (use
`patchFile` / `next.build()` / depend on local CLI output) declare
`skipDeployment: true` and early-return on the `skipped` boolean. Suites
where Vercel's edge mutates URLs (`&dpl=`, immutable assets) declare
`disableAutoSkewProtection: true`.

4. **`next.browser({ baseUrl })`.** A handful of tests
(`prerender-export`, `cdn-cache-busting`, `preload-viewport`, both
`react-virtualized` suites) need to drive a separate server (a
static-export server or an `http-proxy` instance) rather than the
Next.js process. Instead of importing `next-webdriver` directly, those
tests now pass `{ baseUrl: <port|url> }` to `next.browser()`. For the
proxy cases, the proxy was moved into `server.js` inside the fixture and
`http-proxy` declared via the `dependencies` option of `nextTestSetup`,
so the test runs with a fully isolated dependency graph.

5. **CI clean-up.** With `test/integration` gone, the `test
integration*` jobs and `integration-tests-manifest`-related logic in
`.github/workflows/build_and_test.yml` were removed, and `run-tests.js`
no longer has the `integration` test-folder branch.

6. **Validation.** The PR was iterated against multiple full CI runs;
the remaining failures on the latest run are pre-existing flakes
(segment-cache 60s `act` timeouts in turbopack-prod) or transient
infrastructure issues unrelated to the conversion.
2026-05-11 13:55:47 +02:00

3675 lines
116 KiB
TypeScript

import http from 'http'
import stripAnsi from 'strip-ansi'
import cheerio from 'cheerio'
import WebSocket from 'ws'
import {
waitForNoRedbox,
findPort,
getBrowserBodyText,
waitFor,
normalizeRegEx,
normalizeManifest,
retry,
} from 'next-test-utils'
import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils'
describe('Custom routes', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipStart: true,
disableAutoSkewProtection: true,
skipDeployment: true,
})
if (skipped) return
let externalServerPort: number
let externalServer: http.Server
let externalServerHits: Set<string>
let buildId: string
let buildCliOutput: string = ''
beforeEach(() => {
externalServerHits = new Set()
})
beforeAll(async () => {
externalServerPort = await findPort()
externalServer = http.createServer((req, res) => {
externalServerHits.add(req.url)
const nextHost = req.headers['x-forwarded-host']
const externalHost = req.headers['host']
res.end(`hi ${nextHost} from ${externalHost}`)
})
const wsServer = new WebSocket.Server({ noServer: true })
externalServer.on('upgrade', (req, socket, head) => {
externalServerHits.add(req.url)
wsServer.handleUpgrade(req, socket, head, (client) => {
client.send('hello world')
})
})
await new Promise<void>((resolve, reject) => {
const onError = (error: Error) => {
reject(error)
}
externalServer.once('error', onError)
externalServer.listen(externalServerPort, () => {
externalServer.off('error', onError)
resolve()
})
})
await next.patchFile('next.config.js', (content) =>
content.replace(/__EXTERNAL_PORT__/g, String(externalServerPort))
)
if (!isNextDev) {
const result = await next.build({ args: ['-d'] })
buildCliOutput = result.cliOutput || ''
buildId = (await next.readFile('.next/BUILD_ID')).trim()
await next.start({ skipBuild: true })
} else {
buildId = 'development'
await next.start()
}
})
afterAll(() => {
externalServer.close()
})
it.each([
{
path: '/to-ANOTHER',
content: /could not be found/,
status: 404,
},
{
path: '/HELLO-world',
content: /could not be found/,
status: 404,
},
{
path: '/docs/GITHUB',
content: /could not be found/,
status: 404,
},
{
path: '/add-HEADER',
content: /could not be found/,
status: 404,
},
])(
'should honor caseSensitiveRoutes config for $path',
async ({ path, status, content }) => {
const res = await next.fetch(path, { redirect: 'manual' })
if (status) {
expect(res.status).toBe(status)
}
if (content) {
expect(await res.text()).toMatch(content)
}
}
)
it('should successfully rewrite a WebSocket request', async () => {
const messages = []
const ws = await new Promise<WebSocket>((resolve, reject) => {
let socket = new WebSocket(`ws://localhost:${next.appPort}/to-websocket`)
socket.on('message', (data) => {
messages.push(data.toString())
})
socket.on('open', () => resolve(socket))
socket.on('error', (err) => {
console.error(err)
socket.close()
reject()
})
})
await retry(() => {
expect(messages.length).toBeGreaterThan(0)
})
ws.close()
expect([...externalServerHits]).toEqual(['/_next/hmr?page=/about'])
})
it('should successfully rewrite a WebSocket request to a page', async () => {
const messages = []
try {
const ws = await new Promise<WebSocket>((resolve, reject) => {
let socket = new WebSocket(
`ws://localhost:${next.appPort}/websocket-to-page`
)
socket.on('message', (data) => {
messages.push(data.toString())
})
socket.on('open', () => resolve(socket))
socket.on('error', (err) => {
console.error(err)
socket.close()
reject()
})
})
ws.close()
} catch (err) {
messages.push(err)
}
expect(next.cliOutput).not.toContain('unhandledRejection')
})
it('should not rewrite for _next/data route when a match is found', async () => {
const initial = await next.fetch('/overridden/first')
expect(initial.status).toBe(200)
expect(await initial.text()).toContain('this page is overridden')
const nextData = await next.fetch(
`/_next/data/${buildId}/overridden/first.json`
)
expect(nextData.status).toBe(200)
expect(await nextData.json()).toEqual({
pageProps: { params: { slug: 'first' } },
__N_SSG: true,
})
})
it('should handle has query encoding correctly', async () => {
for (const expected of [
{
post: 'first',
slug: ['first'],
},
{
post: 'hello%20world',
slug: ['hello world'],
},
{
post: 'hello/world',
slug: ['hello', 'world'],
},
{
post: 'hello%2fworld',
slug: ['hello', 'world'],
},
]) {
const { post } = expected
const res = await next.fetch(`/has-rewrite-8?post=${post}`, {
redirect: 'manual',
})
expect(res.status).toBe(200)
const $ = cheerio.load(await res.text())
expect(JSON.parse($('#props').text())).toEqual({
params: {
slug: expected.slug,
},
})
}
})
it('should handle external beforeFiles rewrite correctly', async () => {
const res = await next.fetch('/overridden')
const html = await res.text()
if (res.status !== 200) {
console.error('Invalid response', html)
}
expect(res.status).toBe(200)
expect(html).toContain('Example Domain')
const browser = await next.browser('/nav')
await browser.elementByCss('#to-before-files-overridden').click()
await retry(async () => {
expect(await browser.eval('document.documentElement.innerHTML')).toMatch(
/Example Domain/
)
})
})
it('should handle beforeFiles rewrite to dynamic route correctly', async () => {
const res = await next.fetch('/nfl')
const html = await res.text()
if (res.status !== 200) {
console.error('Invalid response', html)
}
expect(res.status).toBe(200)
expect(html).toContain('/_sport/[slug]')
const browser = await next.browser('/nav')
await browser.eval('window.beforeNav = 1')
await browser.elementByCss('#to-before-files-dynamic').click()
await retry(async () => {
expect(await browser.eval('document.documentElement.innerHTML')).toMatch(
/_sport\/\[slug\]/
)
})
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
slug: 'nfl',
})
expect(await browser.elementByCss('#pathname').text()).toBe(
'/_sport/[slug]'
)
expect(await browser.eval('window.beforeNav')).toBe(1)
})
it('should handle beforeFiles rewrite to partly dynamic route correctly', async () => {
const res = await next.fetch('/nfl')
const html = await res.text()
if (res.status !== 200) {
console.error('Invalid response', html)
}
expect(res.status).toBe(200)
expect(html).toContain('/_sport/[slug]')
const browser = await next.browser('/nav')
await browser.eval('window.beforeNav = 1')
await browser.elementByCss('#to-before-files-dynamic-again').click()
await retry(async () => {
expect(await browser.eval('document.documentElement.innerHTML')).toMatch(
/_sport\/\[slug\]\/test/
)
})
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
slug: 'nfl',
})
expect(await browser.elementByCss('#pathname').text()).toBe(
'/_sport/[slug]/test'
)
expect(await browser.eval('window.beforeNav')).toBe(1)
})
it('should support long URLs for rewrites', async () => {
const res = await next.fetch(
'/catchall-rewrite/a9btBxtHQALZ6cxfuj18X6OLGNSkJVzrOXz41HG4QwciZfn7ggRZzPx21dWqGiTBAqFRiWvVNm5ko2lpyso5jtVaXg88dC1jKfqI2qmIcdeyJat8xamrIh2LWnrYRrsBcoKfQU65KHod8DPANuzPS3fkVYWlmov05GQbc82HwR1exOvPVKUKb5gBRWiN0WOh7hN4QyezIuq3dJINAptFQ6m2bNGjYACBRk4MOSHdcQG58oq5Ch7luuqrl9EcbWSa'
)
const html = await res.text()
expect(res.status).toBe(200)
expect(html).toContain('/with-params')
})
it('should resolveHref correctly navigating through history', async () => {
const browser = await next.browser('/')
await browser.eval('window.beforeNav = 1')
expect(await browser.eval('document.documentElement.innerHTML')).toContain(
'multi-rewrites'
)
await browser.eval('next.router.push("/rewriting-to-auto-export")')
await browser.waitForElementByCss('#auto-export')
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
slug: 'hello',
rewrite: '1',
})
expect(await browser.eval('window.beforeNav')).toBe(1)
await browser.eval('next.router.push("/nav")')
await browser.waitForElementByCss('#nav')
expect(await browser.elementByCss('#nav').text()).toBe('Nav')
await browser.back()
await browser.waitForElementByCss('#auto-export')
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
slug: 'hello',
rewrite: '1',
})
expect(await browser.eval('window.beforeNav')).toBe(1)
if (isNextDev) {
await waitForNoRedbox(browser)
}
})
it('should continue in beforeFiles rewrites', async () => {
const res = await next.fetch('/old-blog/about')
expect(res.status).toBe(200)
const html = await res.text()
const $ = cheerio.load(html)
expect($('#hello').text()).toContain('Hello')
const browser = await next.browser('/nav')
await browser.eval('window.beforeNav = 1')
await browser
.elementByCss('#to-old-blog')
.click()
.waitForElementByCss('#hello')
expect(await browser.elementByCss('#hello').text()).toContain('Hello')
})
it('should not hang when proxy rewrite fails', async () => {
const res = await next.fetch('/to-nowhere', {
timeout: 5000,
})
expect(res.status).toBe(500)
})
it('should parse params correctly for rewrite to auto-export dynamic page', async () => {
const browser = await next.browser('/rewriting-to-auto-export')
await retry(async () => {
expect(
await browser.eval(() => document.documentElement.innerHTML)
).toMatch(/auto-export.*?hello/)
})
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
rewrite: '1',
slug: 'hello',
})
})
it('should provide params correctly for rewrite to auto-export non-dynamic page', async () => {
const browser = await next.browser(
'/rewriting-to-another-auto-export/first'
)
expect(await browser.elementByCss('#auto-export-another').text()).toBe(
'auto-export another'
)
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
rewrite: '1',
path: ['first'],
})
})
it('should handle one-to-one rewrite successfully', async () => {
const html = await next.render('/first')
expect(html).toMatch(/hello/)
})
it('should handle chained rewrites successfully', async () => {
const html = await next.render('/')
expect(html).toMatch(/multi-rewrites/)
})
it('should handle param like headers properly', async () => {
const res = await next.fetch('/my-other-header/my-path')
expect(res.headers.get('x-path')).toBe('my-path')
expect(res.headers.get('somemy-path')).toBe('hi')
expect(res.headers.get('x-test')).toBe('some:value*')
expect(res.headers.get('x-test-2')).toBe('value*')
expect(res.headers.get('x-test-3')).toBe(':value?')
expect(res.headers.get('x-test-4')).toBe(':value+')
expect(res.headers.get('x-test-5')).toBe('something https:')
expect(res.headers.get('x-test-6')).toBe(':hello(world)')
expect(res.headers.get('x-test-7')).toBe('hello(world)')
expect(res.headers.get('x-test-8')).toBe('hello{1,}')
expect(res.headers.get('x-test-9')).toBe(':hello{1,2}')
expect(res.headers.get('content-security-policy')).toBe(
"default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com/my-path"
)
})
it('should not match dynamic route immediately after applying header', async () => {
const res = await next.fetch('/blog/post-321')
expect(res.headers.get('x-something')).toBe('applied-everywhere')
const $ = cheerio.load(await res.text())
expect(JSON.parse($('p').text()).path).toBe('blog')
})
it('should handle chained redirects successfully', async () => {
const res1 = await next.fetch('/redir-chain1', { redirect: 'manual' })
const res1location = new URL(res1.headers.get('location'), res1.url)
.pathname
expect(res1.status).toBe(301)
expect(res1location).toBe('/redir-chain2')
const res2 = await next.fetch(res1location, { redirect: 'manual' })
const res2location = new URL(res2.headers.get('location'), res2.url)
.pathname
expect(res2.status).toBe(302)
expect(res2location).toBe('/redir-chain3')
const res3 = await next.fetch(res2location, { redirect: 'manual' })
const res3location = new URL(res3.headers.get('location'), res3.url)
.pathname
expect(res3.status).toBe(303)
expect(res3location).toBe('/')
})
it('should not match redirect for /_next', async () => {
const res = await next.fetch('/_next/has-redirect-5', {
headers: {
'x-test-next': 'true',
},
redirect: 'manual',
})
expect(res.status).toBe(404)
const res2 = await next.fetch('/another/has-redirect-5', {
headers: {
'x-test-next': 'true',
},
redirect: 'manual',
})
expect(res2.status).toBe(307)
})
it('should redirect successfully with permanent: false', async () => {
const res = await next.fetch('/redirect1', { redirect: 'manual' })
const { pathname } = new URL(res.headers.get('location'), res.url)
expect(res.status).toBe(307)
expect(pathname).toBe('/')
})
it('should redirect with params successfully', async () => {
const res = await next.fetch('/hello/123/another', {
redirect: 'manual',
})
const { pathname } = new URL(res.headers.get('location'), res.url)
expect(res.status).toBe(307)
expect(pathname).toBe('/blog/123')
})
it('should redirect with hash successfully', async () => {
const res = await next.fetch('/docs/router-status/500', {
redirect: 'manual',
})
const { pathname, hash, search } = new URL(
res.headers.get('location'),
res.url
)
expect(res.status).toBe(301)
expect(pathname).toBe('/docs/v2/network/status-codes')
expect(hash).toBe('#500')
expect(search).toEqual('')
})
it('should redirect successfully with provided statusCode', async () => {
const res = await next.fetch('/redirect2', { redirect: 'manual' })
const { pathname, search } = new URL(res.headers.get('location'), res.url)
expect(res.status).toBe(301)
expect(pathname).toBe('/')
expect(search).toEqual('')
})
it('should redirect successfully with catchall', async () => {
const res = await next.fetch('/catchall-redirect/hello/world', {
redirect: 'manual',
})
const { pathname, search } = new URL(res.headers.get('location'), res.url)
expect(res.status).toBe(307)
expect(pathname).toBe('/somewhere')
expect(search).toEqual('')
})
it('should server static files through a rewrite', async () => {
const text = await next.render('/hello-world')
expect(text).toBe('hello world!')
})
it('should rewrite with params successfully', async () => {
const html = await next.render('/test/hello')
expect(html).toMatch(/Hello/)
})
it('should not append params when one is used in destination path', async () => {
const html = await next.render('/test/with-params?a=b')
const $ = cheerio.load(html)
expect(JSON.parse($('p').text())).toEqual({ a: 'b' })
})
it('should double redirect successfully', async () => {
const html = await next.render('/docs/github')
expect(html).toMatch(/hi there/)
})
it('should allow params in query for rewrite', async () => {
const html = await next.render('/query-rewrite/hello/world?a=b')
const $ = cheerio.load(html)
expect(JSON.parse($('#__NEXT_DATA__').html()).query).toEqual({
first: 'hello',
second: 'world',
a: 'b',
section: 'hello',
name: 'world',
})
})
it('should have correct params for catchall rewrite', async () => {
const html = await next.render('/catchall-rewrite/hello/world?a=b')
const $ = cheerio.load(html)
expect(JSON.parse($('#__NEXT_DATA__').html()).query).toEqual({
a: 'b',
path: ['hello', 'world'],
})
})
it('should have correct encoding for params with catchall rewrite', async () => {
const html = await next.render(
'/catchall-rewrite/hello%20world%3Fw%3D24%26focalpoint%3Dcenter?a=b'
)
const $ = cheerio.load(html)
expect(JSON.parse($('#__NEXT_DATA__').html()).query).toEqual({
a: 'b',
path: ['hello%20world%3Fw%3D24%26focalpoint%3Dcenter'],
})
})
it('should have correct query for catchall rewrite', async () => {
const html = await next.render('/catchall-query/hello/world?a=b')
const $ = cheerio.load(html)
expect(JSON.parse($('#__NEXT_DATA__').html()).query).toEqual({
a: 'b',
another: 'hello/world',
path: ['hello', 'world'],
})
})
it('should have correct header for catchall rewrite', async () => {
const res = await next.fetch('/catchall-header/hello/world?a=b')
const headerValue = res.headers.get('x-value')
expect(headerValue).toBe('hello/world')
})
it('should allow params in query for redirect', async () => {
const res = await next.fetch('/query-redirect/hello/world?a=b', {
redirect: 'manual',
})
const { pathname, searchParams } = new URL(
res.headers.get('location'),
res.url
)
expect(res.status).toBe(307)
expect(pathname).toBe('/with-params')
expect(Object.fromEntries(searchParams)).toEqual({
first: 'hello',
second: 'world',
a: 'b',
})
})
it('should have correctly encoded params in query for redirect', async () => {
const res = await next.fetch(
'/query-redirect/hello%20world%3Fw%3D24%26focalpoint%3Dcenter/world?a=b',
{ redirect: 'manual' }
)
const { pathname, searchParams } = new URL(
res.headers.get('location'),
res.url
)
expect(res.status).toBe(307)
expect(pathname).toBe('/with-params')
expect(Object.fromEntries(searchParams)).toEqual({
first: 'hello world?w=24&focalpoint=center',
second: 'world',
a: 'b',
})
})
it('should overwrite param values correctly', async () => {
const html = await next.render('/test-overwrite/first/second')
expect(html).toMatch(/this-should-be-the-value/)
expect(html).not.toMatch(/first/)
expect(html).toMatch(/second/)
})
it('should handle query for rewrite correctly', async () => {
const html = await next.render(
'/query-rewrite/first/second?section=overridden&name=overridden&first=overridden&second=overridden&keep=me'
)
const data = JSON.parse(cheerio.load(html)('p').text())
expect(data).toEqual({
first: 'first',
second: 'second',
section: 'first',
name: 'second',
keep: 'me',
})
})
it('should not allow rewrite to override page file', async () => {
const html = await next.render('/nav')
expect(html).toContain('to-hello')
})
it('show allow redirect to override the page', async () => {
const res = await next.fetch('/redirect-override', { redirect: 'manual' })
const { pathname } = new URL(res.headers.get('location') || '', res.url)
expect(res.status).toBe(307)
expect(pathname).toBe('/thank-you-next')
})
it('should work successfully on the client', async () => {
const browser = await next.browser('/nav')
await browser.elementByCss('#to-hello').click()
await browser.waitForElementByCss('#hello')
expect(await browser.eval('window.location.href')).toMatch(/\/first$/)
expect(await getBrowserBodyText(browser)).toMatch(/Hello/)
await browser.eval('window.location.href = window.location.href')
await waitFor(500)
expect(await browser.eval('window.location.href')).toMatch(/\/first$/)
expect(await getBrowserBodyText(browser)).toMatch(/Hello/)
await browser.elementByCss('#to-nav').click()
await browser.waitForElementByCss('#to-hello-again')
await browser.elementByCss('#to-hello-again').click()
await browser.waitForElementByCss('#hello-again')
expect(await browser.eval('window.location.href')).toMatch(/\/second$/)
expect(await getBrowserBodyText(browser)).toMatch(/Hello again/)
await browser.eval('window.location.href = window.location.href')
await waitFor(500)
expect(await browser.eval('window.location.href')).toMatch(/\/second$/)
expect(await getBrowserBodyText(browser)).toMatch(/Hello again/)
})
it('should work with rewrite when manually specifying href/as', async () => {
const browser = await next.browser('/nav')
await browser.eval('window.beforeNav = 1')
await browser
.elementByCss('#to-params-manual')
.click()
.waitForElementByCss('#query')
expect(await browser.eval('window.beforeNav')).toBe(1)
const query = JSON.parse(await browser.elementByCss('#query').text())
expect(query).toEqual({
something: '1',
another: 'value',
})
})
it('should work with rewrite when only specifying href', async () => {
const browser = await next.browser('/nav')
await browser.eval('window.beforeNav = 1')
await browser
.elementByCss('#to-params')
.click()
.waitForElementByCss('#query')
expect(await browser.eval('window.beforeNav')).toBe(1)
const query = JSON.parse(await browser.elementByCss('#query').text())
expect(query).toEqual({
something: '1',
another: 'value',
})
})
it('should work with rewrite when only specifying href and ends in dynamic route', async () => {
const browser = await next.browser('/nav')
await browser.eval('window.beforeNav = 1')
await browser
.elementByCss('#to-rewritten-dynamic')
.click()
.waitForElementByCss('#auto-export')
expect(await browser.eval('window.beforeNav')).toBe(1)
const text = await browser.eval(() => document.documentElement.innerHTML)
expect(text).toContain('auto-export hello')
})
it('should match a page after a rewrite', async () => {
const html = await next.render('/to-hello')
expect(html).toContain('Hello')
})
it('should match dynamic route after rewrite', async () => {
const html = await next.render('/blog/post-1')
expect(html).toMatch(/post:.*?post-2/)
})
it('should match public file after rewrite', async () => {
const data = await next.render('/blog/data.json')
expect(JSON.parse(data)).toEqual({ hello: 'world' })
})
it('should match /_next file after rewrite', async () => {
await next.render('/hello')
const data = await next.render(
`/hidden/_next/static/${buildId}/_buildManifest.js`
)
expect(data).toContain('/hello')
})
it('should allow redirecting to external resource', async () => {
const res = await next.fetch('/to-external', { redirect: 'manual' })
const location = res.headers.get('location')
expect(res.status).toBe(307)
expect(location).toBe('https://google.com/')
})
it('should apply headers for exact match', async () => {
const res = await next.fetch('/add-header')
expect(res.headers.get('x-custom-header')).toBe('hello world')
expect(res.headers.get('x-another-header')).toBe('hello again')
})
it('should apply headers for multi match', async () => {
const res = await next.fetch('/my-headers/first')
expect(res.headers.get('x-first-header')).toBe('first')
expect(res.headers.get('x-second-header')).toBe('second')
})
it('should apply params for header key/values', async () => {
const res = await next.fetch('/my-other-header/first')
expect(res.headers.get('x-path')).toBe('first')
expect(res.headers.get('somefirst')).toBe('hi')
})
it('should support URL for header key/values', async () => {
const res = await next.fetch('/without-params/url')
expect(res.headers.get('x-origin')).toBe('https://example.com')
})
it('should apply params header key/values with URL', async () => {
const res = await next.fetch('/with-params/url/first')
expect(res.headers.get('x-url')).toBe('https://example.com/first')
})
it('should apply params header key/values with URL that has port', async () => {
const res = await next.fetch('/with-params/url2/first')
expect(res.headers.get('x-url')).toBe(
'https://example.com:8080?hello=first'
)
})
it('should support named pattern for header key/values', async () => {
const res = await next.fetch('/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 next.fetch('/proxy-me/first?keep=me&and=me')
expect(res.status).toBe(200)
expect(
[...externalServerHits].map((u) => {
const { pathname, searchParams } = new URL(u, res.url)
return {
pathname,
query: Object.fromEntries(searchParams),
}
})
).toEqual([
{
pathname: '/first',
query: {
keep: 'me',
and: 'me',
},
},
])
const nextHost = `localhost:${next.appPort}`
const externalHost = `localhost:${externalServerPort}`
expect(await res.text()).toContain(`hi ${nextHost} from ${externalHost}`)
})
it('should support unnamed parameters correctly', async () => {
const res = await next.fetch('/unnamed/first/final', {
redirect: 'manual',
})
const { pathname } = new URL(res.headers.get('location') || '', res.url)
expect(res.status).toBe(307)
expect(pathname).toBe('/got-unnamed')
})
it('should support named like unnamed parameters correctly', async () => {
const res = await next.fetch('/named-like-unnamed/first', {
redirect: 'manual',
})
const { pathname } = new URL(res.headers.get('location') || '', res.url)
expect(res.status).toBe(307)
expect(pathname).toBe('/first')
})
it('should add refresh header for 308 redirect', async () => {
const res = await next.fetch('/redirect4', { redirect: 'manual' })
expect(res.status).toBe(308)
expect(res.headers.get('refresh')).toBe(`0;url=/`)
})
it('should have correctly encoded query in location and refresh headers', async () => {
const res = await next.fetch(
'/redirect4?%E3%83%86%E3%82%B9%E3%83%88=%E3%81%82',
{ redirect: 'manual' }
)
expect(res.status).toBe(308)
expect(res.headers.get('location').split('?')[1]).toBe(
'%E3%83%86%E3%82%B9%E3%83%88=%E3%81%82'
)
expect(res.headers.get('refresh')).toBe(
'0;url=/?%E3%83%86%E3%82%B9%E3%83%88=%E3%81%82'
)
})
it('should handle basic api rewrite successfully', async () => {
const data = await next.render('/api-hello')
expect(JSON.parse(data)).toEqual({ query: {} })
})
it('should handle api rewrite with un-named param successfully', async () => {
const data = await next.render('/api-hello-regex/hello/world')
expect(JSON.parse(data)).toEqual({
query: { name: 'hello/world', first: 'hello/world' },
})
})
it('should handle api rewrite with param successfully', async () => {
const data = await next.render('/api-hello-param/hello')
expect(JSON.parse(data)).toEqual({
query: { name: 'hello', hello: 'hello' },
})
})
it('should handle encoded value in the pathname correctly', async () => {
const res = await next.fetch(
'/redirect/me/to-about/' + encodeURI('\\google.com'),
{ redirect: 'manual' }
)
const { pathname, hostname, searchParams } = new URL(
res.headers.get('location') || '',
res.url
)
expect(res.status).toBe(307)
expect(pathname).toBe(encodeURI('/\\google.com/about'))
expect(hostname).not.toBe('google.com')
expect(Object.fromEntries(searchParams)).toEqual({})
})
it('should handle unnamed parameters with multi-match successfully', async () => {
const html = await next.render(
'/unnamed-params/nested/first/second/hello/world'
)
const params = JSON.parse(cheerio.load(html)('p').text())
expect(params).toEqual({ test: 'hello' })
})
it('should handle named regex parameters with multi-match successfully', async () => {
const res = await next.fetch('/docs/integrations/v2-some/thing', {
redirect: 'manual',
})
const { pathname } = new URL(res.headers.get('location') || '', res.url)
expect(res.status).toBe(307)
expect(pathname).toBe('/integrations/-some/thing')
})
it('should redirect with URL in query correctly', async () => {
const res = await next.fetch('/to-external-with-query', {
redirect: 'manual',
})
expect(res.status).toBe(307)
expect(res.headers.get('location')).toBe(
'https://authserver.example.com/set-password?returnUrl=https://www.example.com/login'
)
})
it('should redirect with URL in query correctly non-encoded', async () => {
const res = await next.fetch('/to-external-with-query', {
redirect: 'manual',
})
expect(res.status).toBe(307)
expect(res.headers.get('location')).toBe(
'https://authserver.example.com/set-password?returnUrl=https://www.example.com/login'
)
})
it('should match missing header headers correctly', async () => {
const res = await next.fetch('/missing-headers-1', {
headers: {
'x-my-header': 'hello world!!',
},
})
expect(res.status).toBe(404)
const res2 = await next.fetch('/missing-headers-1', {
redirect: 'manual',
})
expect(res2.headers.get('x-new-header')).toBe('new-value')
})
it('should match missing query headers correctly', async () => {
const res = await next.fetch('/missing-headers-2?my-query=hellooo')
expect(res.status).toBe(404)
const res2 = await next.fetch('/missing-headers-2', {
redirect: 'manual',
})
expect(res2.headers.get('x-new-header')).toBe('new-value')
})
it('should match missing cookie headers correctly', async () => {
const res = await next.fetch('/missing-headers-3', {
headers: {
cookie: 'loggedIn=true',
},
redirect: 'manual',
})
expect(res.status).toBe(404)
const res2 = await next.fetch('/missing-headers-3', {
redirect: 'manual',
})
expect(res2.headers.get('x-new-header')).toBe('new-value')
})
it('should match missing header redirect correctly', async () => {
const res = await next.fetch('/missing-rewrite-1', {
headers: {
'x-my-header': 'hello world!!',
},
})
expect(res.status).toBe(404)
const res2 = await next.fetch('/missing-redirect-1', {
redirect: 'manual',
})
expect(res2.status).toBe(307)
const url = new URL(res2.headers.get('location'), 'http://n')
expect(url.pathname).toBe('/with-params')
})
it('should match missing query redirect correctly', async () => {
const res = await next.fetch('/missing-redirect-2?my-query=hellooo')
expect(res.status).toBe(404)
const res2 = await next.fetch('/missing-redirect-2', {
redirect: 'manual',
})
expect(res2.status).toBe(307)
const url = new URL(res2.headers.get('location'), 'http://n')
expect(url.pathname).toBe('/with-params')
})
it('should match missing cookie redirect correctly', async () => {
const res = await next.fetch('/missing-redirect-3', {
headers: {
cookie: 'loggedIn=true',
},
redirect: 'manual',
})
expect(res.status).toBe(404)
const res2 = await next.fetch('/missing-redirect-3', {
redirect: 'manual',
})
expect(res2.status).toBe(307)
const url = new URL(res2.headers.get('location'), 'http://n')
expect(url.pathname).toBe('/with-params')
expect(url.search).toBe('?authorized=1')
})
it('should match missing header rewrite correctly', async () => {
const res = await next.fetch('/missing-rewrite-1', {
headers: {
'x-my-header': 'hello world!!',
},
})
expect(res.status).toBe(404)
const res2 = await next.fetch('/missing-rewrite-1')
const $2 = cheerio.load(await res2.text())
expect(res2.status).toBe(200)
expect(JSON.parse($2('#query').text())).toEqual({})
})
it('should match missing query rewrite correctly', async () => {
const res = await next.fetch('/missing-rewrite-2?my-query=hellooo')
expect(res.status).toBe(404)
const res2 = await next.fetch('/missing-rewrite-2')
const $2 = cheerio.load(await res2.text())
expect(res2.status).toBe(200)
expect(JSON.parse($2('#query').text())).toEqual({})
})
it('should match missing cookie rewrite correctly', async () => {
const res = await next.fetch('/missing-rewrite-3', {
headers: {
cookie: 'loggedIn=true',
},
})
expect(res.status).toBe(404)
const res2 = await next.fetch('/missing-rewrite-3')
const $2 = cheerio.load(await res2.text())
expect(JSON.parse($2('#query').text())).toEqual({
authorized: '1',
})
expect(res2.status).toBe(200)
})
it('should match has header rewrite correctly', async () => {
const res = await next.fetch('/has-rewrite-1', {
headers: {
'x-my-header': 'hello world!!',
},
})
expect(res.status).toBe(200)
const $ = cheerio.load(await res.text())
expect(JSON.parse($('#query').text())).toEqual({
myHeader: 'hello world!!',
})
const res2 = await next.fetch('/has-rewrite-1')
expect(res2.status).toBe(404)
})
it('should match has query rewrite correctly', async () => {
const res = await next.fetch('/has-rewrite-2?my-query=hellooo')
expect(res.status).toBe(200)
const $ = cheerio.load(await res.text())
expect(JSON.parse($('#query').text())).toEqual({
'my-query': 'hellooo',
myquery: 'hellooo',
value: 'hellooo',
})
const res2 = await next.fetch('/has-rewrite-2')
expect(res2.status).toBe(404)
})
it('should match has cookie rewrite correctly', async () => {
const res = await next.fetch('/has-rewrite-3', {
headers: {
cookie: 'loggedIn=true',
},
})
expect(res.status).toBe(200)
const $ = cheerio.load(await res.text())
expect(JSON.parse($('#query').text())).toEqual({
loggedIn: 'true',
authorized: '1',
})
const res2 = await next.fetch('/has-rewrite-3')
expect(res2.status).toBe(404)
})
it('should match has host rewrite correctly', async () => {
const res1 = await next.fetch('/has-rewrite-4')
expect(res1.status).toBe(404)
const res = await next.fetch('/has-rewrite-4', {
headers: {
host: 'example.com',
},
})
expect(res.status).toBe(200)
const $ = cheerio.load(await res.text())
expect(JSON.parse($('#query').text())).toEqual({
host: '1',
})
const res2 = await next.fetch('/has-rewrite-4')
expect(res2.status).toBe(404)
})
it('should pass has segment for rewrite correctly', async () => {
const res1 = await next.fetch('/has-rewrite-5')
expect(res1.status).toBe(404)
const res = await next.fetch('/has-rewrite-5?hasParam=with-params')
expect(res.status).toBe(200)
const $ = cheerio.load(await res.text())
expect(JSON.parse($('#query').text())).toEqual({
hasParam: 'with-params',
})
})
it('should not pass non captured has value for rewrite correctly', async () => {
const res1 = await next.fetch('/has-rewrite-6')
expect(res1.status).toBe(404)
const res = await next.fetch('/has-rewrite-6', {
headers: {
hasParam: 'with-params',
},
})
expect(res.status).toBe(200)
const $ = cheerio.load(await res.text())
expect(JSON.parse($('#query').text())).toEqual({})
})
it('should pass captured has value for rewrite correctly', async () => {
const res1 = await next.fetch('/has-rewrite-7')
expect(res1.status).toBe(404)
const res = await next.fetch('/has-rewrite-7?hasParam=with-params')
expect(res.status).toBe(200)
const $ = cheerio.load(await res.text())
expect(JSON.parse($('#query').text())).toEqual({
hasParam: 'with-params',
idk: 'with-params',
})
})
it('should match has rewrite correctly before files', async () => {
const res1 = await next.fetch('/hello')
expect(res1.status).toBe(200)
const $1 = cheerio.load(await res1.text())
expect($1('#hello').text()).toBe('Hello')
const res = await next.fetch('/hello?overrideMe=1')
expect(res.status).toBe(200)
const $ = cheerio.load(await res.text())
expect(JSON.parse($('#query').text())).toEqual({
overrideMe: '1',
overridden: '1',
})
const browser = await next.browser('/nav')
await browser.eval('window.beforeNav = 1')
await browser.elementByCss('#to-overridden').click()
await browser.waitForElementByCss('#query')
expect(await browser.eval('window.next.router.pathname')).toBe(
'/with-params'
)
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
overridden: '1',
overrideMe: '1',
})
expect(await browser.eval('window.beforeNav')).toBe(1)
})
it('should match has header redirect correctly', async () => {
const res = await next.fetch('/has-redirect-1', {
headers: {
'x-my-header': 'hello world!!',
},
redirect: 'manual',
})
expect(res.status).toBe(307)
const parsed = new URL(res.headers.get('location'), res.url)
expect(parsed.pathname).toBe('/another')
expect(Object.fromEntries(parsed.searchParams)).toEqual({
myHeader: 'hello world!!',
})
const res2 = await next.fetch('/has-redirect-1', { redirect: 'manual' })
expect(res2.status).toBe(404)
})
it('should match has query redirect correctly', async () => {
const res = await next.fetch('/has-redirect-2?my-query=hellooo', {
redirect: 'manual',
})
expect(res.status).toBe(307)
const parsed = new URL(res.headers.get('location'), res.url)
expect(parsed.pathname).toBe('/another')
expect(Object.fromEntries(parsed.searchParams)).toEqual({
value: 'hellooo',
'my-query': 'hellooo',
})
const res2 = await next.fetch('/has-redirect-2', { redirect: 'manual' })
expect(res2.status).toBe(404)
})
it('should match has cookie redirect correctly', async () => {
const res = await next.fetch('/has-redirect-3', {
headers: {
cookie: 'loggedIn=true',
},
redirect: 'manual',
})
expect(res.status).toBe(307)
const parsed = new URL(res.headers.get('location'), res.url)
expect(parsed.pathname).toBe('/another')
expect(Object.fromEntries(parsed.searchParams)).toEqual({
authorized: '1',
})
const res2 = await next.fetch('/has-redirect-3', { redirect: 'manual' })
expect(res2.status).toBe(404)
})
it('should match has host redirect correctly', async () => {
const res1 = await next.fetch('/has-redirect-4', { redirect: 'manual' })
expect(res1.status).toBe(404)
const res = await next.fetch('/has-redirect-4', {
headers: {
host: 'example.com',
},
redirect: 'manual',
})
expect(res.status).toBe(307)
const parsed = new URL(res.headers.get('location'), res.url)
expect(parsed.pathname).toBe('/another')
expect(Object.fromEntries(parsed.searchParams)).toEqual({
host: '1',
})
})
it('should match has host redirect and insert in destination correctly', async () => {
const res1 = await next.fetch('/has-redirect-6', { redirect: 'manual' })
expect(res1.status).toBe(404)
const res = await next.fetch('/has-redirect-6', {
headers: {
host: 'hello-test.example.com',
},
redirect: 'manual',
})
expect(res.status).toBe(307)
const parsed = new URL(res.headers.get('location'), res.url)
expect(parsed.protocol).toBe('https:')
expect(parsed.hostname).toBe('hello.example.com')
expect(parsed.pathname).toBe('/some-path/end')
expect(Object.fromEntries(parsed.searchParams)).toEqual({
a: 'b',
})
})
it('should match has query redirect with duplicate query key', async () => {
const res = await next.fetch('/has-redirect-7?hello=world&hello=another', {
redirect: 'manual',
})
expect(res.status).toBe(307)
const parsed = new URL(res.headers.get('location'), res.url)
expect(parsed.pathname).toBe('/somewhere')
const query = {}
for (const key of parsed.searchParams.keys()) {
const values = parsed.searchParams.getAll(key)
query[key] = values.length > 1 ? values : values[0]
}
expect(query).toEqual({
hello: ['world', 'another'],
value: 'another',
})
})
it('should match has header for header correctly', async () => {
const res = await next.fetch('/has-header-1', {
headers: {
'x-my-header': 'hello world!!',
},
redirect: 'manual',
})
expect(res.headers.get('x-another')).toBe('header')
const res2 = await next.fetch('/has-header-1', { redirect: 'manual' })
expect(res2.headers.get('x-another')).toBe(null)
})
it('should match has query for header correctly', async () => {
const res = await next.fetch('/has-header-2?my-query=hellooo', {
redirect: 'manual',
})
expect(res.headers.get('x-added')).toBe('value')
const res2 = await next.fetch('/has-header-2', { redirect: 'manual' })
expect(res2.headers.get('x-another')).toBe(null)
})
it('should match has cookie for header correctly', async () => {
const res = await next.fetch('/has-header-3', {
headers: {
cookie: 'loggedIn=true',
},
redirect: 'manual',
})
expect(res.headers.get('x-is-user')).toBe('yuuuup')
const res2 = await next.fetch('/has-header-3', { redirect: 'manual' })
expect(res2.headers.get('x-is-user')).toBe(null)
})
it('should match has host for header correctly', async () => {
const res = await next.fetch('/has-header-4', {
headers: {
host: 'example.com',
},
redirect: 'manual',
})
expect(res.headers.get('x-is-host')).toBe('yuuuup')
const res2 = await next.fetch('/has-header-4', { redirect: 'manual' })
expect(res2.headers.get('x-is-host')).toBe(null)
})
if (!isNextDev) {
it('should output routes-manifest successfully', async () => {
const manifest = JSON.parse(
await next.readFile('.next/routes-manifest.json')
)
for (const route of [
...manifest.dynamicRoutes,
...manifest.rewrites.beforeFiles,
...manifest.rewrites.afterFiles,
...manifest.rewrites.fallback,
...manifest.redirects,
...manifest.headers,
]) {
route.regex = normalizeRegEx(route.regex)
}
for (const route of manifest.dataRoutes) {
route.dataRouteRegex = normalizeRegEx(route.dataRouteRegex)
}
const normalizedManifest = normalizeManifest(manifest, [
[buildId, 'BUILD_ID'],
[`${externalServerPort}`, 'EXTERNAL_SERVER_PORT'],
])
if (process.env.__NEXT_CACHE_COMPONENTS === 'true') {
expect(normalizedManifest).toMatchInlineSnapshot(`
{
"appType": "pages",
"basePath": "",
"caseSensitive": true,
"dataRoutes": [
{
"dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/blog\\-catchall\\/(.+?)\\.json$",
"namedDataRouteRegex": "^/_next/data/BUILD_ID/blog\\-catchall/(?<nxtPslug>.+?)\\.json$",
"page": "/blog-catchall/[...slug]",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
{
"dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/overridden\\/([^\\/]+?)\\.json$",
"namedDataRouteRegex": "^/_next/data/BUILD_ID/overridden/(?<nxtPslug>[^/]+?)\\.json$",
"page": "/overridden/[slug]",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
],
"dynamicRoutes": [
{
"namedRegex": "^/_sport/(?<nxtPslug>[^/]+?)(?:/)?$",
"page": "/_sport/[slug]",
"regex": "^\\/_sport\\/([^\\/]+?)(?:\\/)?$",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
{
"namedRegex": "^/_sport/(?<nxtPslug>[^/]+?)/test(?:/)?$",
"page": "/_sport/[slug]/test",
"regex": "^\\/_sport\\/([^\\/]+?)\\/test(?:\\/)?$",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
{
"namedRegex": "^/another/(?<nxtPid>[^/]+?)(?:/)?$",
"page": "/another/[id]",
"regex": "^\\/another\\/([^\\/]+?)(?:\\/)?$",
"routeKeys": {
"nxtPid": "nxtPid",
},
},
{
"namedRegex": "^/api/dynamic/(?<nxtPslug>[^/]+?)(?:/)?$",
"page": "/api/dynamic/[slug]",
"regex": "^\\/api\\/dynamic\\/([^\\/]+?)(?:\\/)?$",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
{
"namedRegex": "^/auto\\-export/(?<nxtPslug>[^/]+?)(?:/)?$",
"page": "/auto-export/[slug]",
"regex": "^\\/auto\\-export\\/([^\\/]+?)(?:\\/)?$",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
{
"namedRegex": "^/blog/(?<nxtPpost>[^/]+?)(?:/)?$",
"page": "/blog/[post]",
"regex": "^\\/blog\\/([^\\/]+?)(?:\\/)?$",
"routeKeys": {
"nxtPpost": "nxtPpost",
},
},
{
"namedRegex": "^/blog\\-catchall/(?<nxtPslug>.+?)(?:/)?$",
"page": "/blog-catchall/[...slug]",
"regex": "^\\/blog\\-catchall\\/(.+?)(?:\\/)?$",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
{
"namedRegex": "^/overridden/(?<nxtPslug>[^/]+?)(?:/)?$",
"page": "/overridden/[slug]",
"regex": "^\\/overridden\\/([^\\/]+?)(?:\\/)?$",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
],
"headers": [
{
"headers": [
{
"key": "x-new-header",
"value": "new-value",
},
],
"missing": [
{
"key": "x-my-header",
"type": "header",
"value": "(?<myHeader>.*)",
},
],
"regex": "^\\/missing-headers-1(?:\\/)?$",
"source": "/missing-headers-1",
},
{
"headers": [
{
"key": "x-new-header",
"value": "new-value",
},
],
"missing": [
{
"key": "my-query",
"type": "query",
},
],
"regex": "^\\/missing-headers-2(?:\\/)?$",
"source": "/missing-headers-2",
},
{
"headers": [
{
"key": "x-new-header",
"value": "new-value",
},
],
"missing": [
{
"key": "loggedIn",
"type": "cookie",
"value": "(?<loggedIn>true)",
},
],
"regex": "^\\/missing-headers-3(?:\\/)?$",
"source": "/missing-headers-3",
},
{
"headers": [
{
"key": "x-custom-header",
"value": "hello world",
},
{
"key": "x-another-header",
"value": "hello again",
},
],
"regex": "^\\/add-header(?:\\/)?$",
"source": "/add-header",
},
{
"headers": [
{
"key": "x-first-header",
"value": "first",
},
{
"key": "x-second-header",
"value": "second",
},
],
"regex": "^\\/my-headers(?:\\/(.*))(?:\\/)?$",
"source": "/my-headers/(.*)",
},
{
"headers": [
{
"key": "x-path",
"value": ":path",
},
{
"key": "some:path",
"value": "hi",
},
{
"key": "x-test",
"value": "some:value*",
},
{
"key": "x-test-2",
"value": "value*",
},
{
"key": "x-test-3",
"value": ":value?",
},
{
"key": "x-test-4",
"value": ":value+",
},
{
"key": "x-test-5",
"value": "something https:",
},
{
"key": "x-test-6",
"value": ":hello(world)",
},
{
"key": "x-test-7",
"value": "hello(world)",
},
{
"key": "x-test-8",
"value": "hello{1,}",
},
{
"key": "x-test-9",
"value": ":hello{1,2}",
},
{
"key": "content-security-policy",
"value": "default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com/:path",
},
],
"regex": "^\\/my-other-header(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/my-other-header/:path",
},
{
"headers": [
{
"key": "x-origin",
"value": "https://example.com",
},
],
"regex": "^\\/without-params\\/url(?:\\/)?$",
"source": "/without-params/url",
},
{
"headers": [
{
"key": "x-url",
"value": "https://example.com/:path*",
},
],
"regex": "^\\/with-params\\/url(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/with-params/url/:path*",
},
{
"headers": [
{
"key": "x-url",
"value": "https://example.com:8080?hello=:path*",
},
],
"regex": "^\\/with-params\\/url2(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/with-params/url2/:path*",
},
{
"headers": [
{
"key": "x-something",
"value": "applied-everywhere",
},
],
"regex": "^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/:path*",
},
{
"headers": [
{
"key": "x-something",
"value": "value=:path",
},
{
"key": "path-:path",
"value": "end",
},
],
"regex": "^\\/named-pattern(?:\\/(.*))(?:\\/)?$",
"source": "/named-pattern/:path(.*)",
},
{
"headers": [
{
"key": "x-value",
"value": ":path*",
},
],
"regex": "^\\/catchall-header(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/catchall-header/:path*",
},
{
"has": [
{
"key": "x-my-header",
"type": "header",
"value": "(?<myHeader>.*)",
},
],
"headers": [
{
"key": "x-another",
"value": "header",
},
],
"regex": "^\\/has-header-1(?:\\/)?$",
"source": "/has-header-1",
},
{
"has": [
{
"key": "my-query",
"type": "query",
},
],
"headers": [
{
"key": "x-added",
"value": "value",
},
],
"regex": "^\\/has-header-2(?:\\/)?$",
"source": "/has-header-2",
},
{
"has": [
{
"key": "loggedIn",
"type": "cookie",
"value": "true",
},
],
"headers": [
{
"key": "x-is-user",
"value": "yuuuup",
},
],
"regex": "^\\/has-header-3(?:\\/)?$",
"source": "/has-header-3",
},
{
"has": [
{
"type": "host",
"value": "example.com",
},
],
"headers": [
{
"key": "x-is-host",
"value": "yuuuup",
},
],
"regex": "^\\/has-header-4(?:\\/)?$",
"source": "/has-header-4",
},
],
"onMatchHeaders": [],
"pages404": true,
"ppr": {
"chain": {
"headers": {
"next-resume": "1",
},
},
},
"redirects": [
{
"destination": "/:path+",
"internal": true,
"priority": true,
"regex": "^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))\\/$",
"source": "/:path+/",
"statusCode": 308,
},
{
"destination": "/with-params",
"missing": [
{
"key": "x-my-header",
"type": "header",
"value": "(?<myHeader>.*)",
},
],
"regex": "^(?!\\/_next)\\/missing-redirect-1(?:\\/)?$",
"source": "/missing-redirect-1",
"statusCode": 307,
},
{
"destination": "/with-params",
"missing": [
{
"key": "my-query",
"type": "query",
},
],
"regex": "^(?!\\/_next)\\/missing-redirect-2(?:\\/)?$",
"source": "/missing-redirect-2",
"statusCode": 307,
},
{
"destination": "/with-params?authorized=1",
"missing": [
{
"key": "loggedIn",
"type": "cookie",
"value": "(?<loggedIn>true)",
},
],
"regex": "^(?!\\/_next)\\/missing-redirect-3(?:\\/)?$",
"source": "/missing-redirect-3",
"statusCode": 307,
},
{
"destination": "/:lang/about",
"regex": "^(?!\\/_next)\\/redirect\\/me\\/to-about(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/redirect/me/to-about/:lang",
"statusCode": 307,
},
{
"destination": "/docs/v2/network/status-codes#:code",
"regex": "^(?!\\/_next)\\/docs\\/router-status(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/docs/router-status/:code",
"statusCode": 301,
},
{
"destination": "/docs/v2/advanced/now-for-github",
"regex": "^(?!\\/_next)\\/docs\\/github(?:\\/)?$",
"source": "/docs/github",
"statusCode": 301,
},
{
"destination": "/docs/v2/more/:all",
"regex": "^(?!\\/_next)\\/docs\\/v2\\/advanced(?:\\/(.*))(?:\\/)?$",
"source": "/docs/v2/advanced/:all(.*)",
"statusCode": 301,
},
{
"destination": "/blog/:id",
"regex": "^(?!\\/_next)\\/hello(?:\\/([^\\/]+?))\\/another(?:\\/)?$",
"source": "/hello/:id/another",
"statusCode": 307,
},
{
"destination": "/",
"regex": "^(?!\\/_next)\\/redirect1(?:\\/)?$",
"source": "/redirect1",
"statusCode": 307,
},
{
"destination": "/",
"regex": "^(?!\\/_next)\\/redirect2(?:\\/)?$",
"source": "/redirect2",
"statusCode": 301,
},
{
"destination": "/another",
"regex": "^(?!\\/_next)\\/redirect3(?:\\/)?$",
"source": "/redirect3",
"statusCode": 302,
},
{
"destination": "/",
"regex": "^(?!\\/_next)\\/redirect4(?:\\/)?$",
"source": "/redirect4",
"statusCode": 308,
},
{
"destination": "/redir-chain2",
"regex": "^(?!\\/_next)\\/redir-chain1(?:\\/)?$",
"source": "/redir-chain1",
"statusCode": 301,
},
{
"destination": "/redir-chain3",
"regex": "^(?!\\/_next)\\/redir-chain2(?:\\/)?$",
"source": "/redir-chain2",
"statusCode": 302,
},
{
"destination": "/",
"regex": "^(?!\\/_next)\\/redir-chain3(?:\\/)?$",
"source": "/redir-chain3",
"statusCode": 303,
},
{
"destination": "https://google.com",
"regex": "^(?!\\/_next)\\/to-external(?:\\/)?$",
"source": "/to-external",
"statusCode": 307,
},
{
"destination": "/with-params?first=:section&second=:name",
"regex": "^(?!\\/_next)\\/query-redirect(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/query-redirect/:section/:name",
"statusCode": 307,
},
{
"destination": "/got-unnamed",
"regex": "^(?!\\/_next)\\/unnamed(?:\\/(first|second))(?:\\/(.*))(?:\\/)?$",
"source": "/unnamed/(first|second)/(.*)",
"statusCode": 307,
},
{
"destination": "/:0",
"regex": "^(?!\\/_next)\\/named-like-unnamed(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/named-like-unnamed/:0",
"statusCode": 307,
},
{
"destination": "/thank-you-next",
"regex": "^(?!\\/_next)\\/redirect-override(?:\\/)?$",
"source": "/redirect-override",
"statusCode": 307,
},
{
"destination": "/:first/:second",
"regex": "^(?!\\/_next)\\/docs(?:\\/(integrations|now-cli))\\/v2(.*)(?:\\/)?$",
"source": "/docs/:first(integrations|now-cli)/v2:second(.*)",
"statusCode": 307,
},
{
"destination": "/somewhere",
"regex": "^(?!\\/_next)\\/catchall-redirect(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/catchall-redirect/:path*",
"statusCode": 307,
},
{
"destination": "https://authserver.example.com/set-password?returnUrl=https%3A%2F%2Fwww.example.com/login",
"regex": "^(?!\\/_next)\\/to-external-with-query(?:\\/)?$",
"source": "/to-external-with-query",
"statusCode": 307,
},
{
"destination": "https://authserver.example.com/set-password?returnUrl=https://www.example.com/login",
"regex": "^(?!\\/_next)\\/to-external-with-query-2(?:\\/)?$",
"source": "/to-external-with-query-2",
"statusCode": 307,
},
{
"destination": "/another?myHeader=:myHeader",
"has": [
{
"key": "x-my-header",
"type": "header",
"value": "(?<myHeader>.*)",
},
],
"regex": "^(?!\\/_next)\\/has-redirect-1(?:\\/)?$",
"source": "/has-redirect-1",
"statusCode": 307,
},
{
"destination": "/another?value=:myquery",
"has": [
{
"key": "my-query",
"type": "query",
},
],
"regex": "^(?!\\/_next)\\/has-redirect-2(?:\\/)?$",
"source": "/has-redirect-2",
"statusCode": 307,
},
{
"destination": "/another?authorized=1",
"has": [
{
"key": "loggedIn",
"type": "cookie",
"value": "true",
},
],
"regex": "^(?!\\/_next)\\/has-redirect-3(?:\\/)?$",
"source": "/has-redirect-3",
"statusCode": 307,
},
{
"destination": "/another?host=1",
"has": [
{
"type": "host",
"value": "example.com",
},
],
"regex": "^(?!\\/_next)\\/has-redirect-4(?:\\/)?$",
"source": "/has-redirect-4",
"statusCode": 307,
},
{
"destination": "/somewhere",
"has": [
{
"key": "x-test-next",
"type": "header",
},
],
"regex": "^(?!\\/_next)(?:\\/([^\\/]+?))\\/has-redirect-5(?:\\/)?$",
"source": "/:path/has-redirect-5",
"statusCode": 307,
},
{
"destination": "https://:subdomain.example.com/some-path/end?a=b",
"has": [
{
"type": "host",
"value": "(?<subdomain>.*)-test.example.com",
},
],
"regex": "^(?!\\/_next)\\/has-redirect-6(?:\\/)?$",
"source": "/has-redirect-6",
"statusCode": 307,
},
{
"destination": "/somewhere?value=:hello",
"has": [
{
"key": "hello",
"type": "query",
"value": "(?<hello>.*)",
},
],
"regex": "^(?!\\/_next)\\/has-redirect-7(?:\\/)?$",
"source": "/has-redirect-7",
"statusCode": 307,
},
],
"rewriteHeaders": {
"pathHeader": "x-nextjs-rewritten-path",
"queryHeader": "x-nextjs-rewritten-query",
},
"rewrites": {
"afterFiles": [
{
"destination": "http://localhost:EXTERNAL_SERVER_PORT/_next/hmr?page=/about",
"regex": "^\\/to-websocket(?:\\/)?$",
"source": "/to-websocket",
},
{
"destination": "/hello",
"regex": "^\\/websocket-to-page(?:\\/)?$",
"source": "/websocket-to-page",
},
{
"destination": "http://localhost:12233",
"regex": "^\\/to-nowhere(?:\\/)?$",
"source": "/to-nowhere",
},
{
"destination": "/auto-export/hello?rewrite=1",
"regex": "^\\/rewriting-to-auto-export(?:\\/)?$",
"source": "/rewriting-to-auto-export",
},
{
"destination": "/auto-export/another?rewrite=1",
"regex": "^\\/rewriting-to-another-auto-export(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/rewriting-to-another-auto-export/:path*",
},
{
"destination": "/another/one",
"regex": "^\\/to-another(?:\\/)?$",
"source": "/to-another",
},
{
"destination": "/404",
"regex": "^\\/nav(?:\\/)?$",
"source": "/nav",
},
{
"destination": "/static/hello.txt",
"regex": "^\\/hello-world(?:\\/)?$",
"source": "/hello-world",
},
{
"destination": "/another",
"regex": "^\\/(?:\\/)?$",
"source": "/",
},
{
"destination": "/multi-rewrites",
"regex": "^\\/another(?:\\/)?$",
"source": "/another",
},
{
"destination": "/hello",
"regex": "^\\/first(?:\\/)?$",
"source": "/first",
},
{
"destination": "/hello-again",
"regex": "^\\/second(?:\\/)?$",
"source": "/second",
},
{
"destination": "/hello",
"regex": "^\\/to-hello(?:\\/)?$",
"source": "/to-hello",
},
{
"destination": "/blog/post-2",
"regex": "^\\/blog\\/post-1(?:\\/)?$",
"source": "/blog/post-1",
},
{
"destination": "/:path",
"regex": "^\\/test(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/test/:path",
},
{
"destination": "/params/this-should-be-the-value",
"regex": "^\\/test-overwrite(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/test-overwrite/:something/:another",
},
{
"destination": "/with-params",
"regex": "^\\/params(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/params/:something",
},
{
"destination": "/with-params?first=:section&second=:name",
"regex": "^\\/query-rewrite(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/query-rewrite/:section/:name",
},
{
"destination": "/_next/:path*",
"regex": "^\\/hidden\\/_next(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/hidden/_next/:path*",
},
{
"destination": "http://localhost:EXTERNAL_SERVER_PORT/:path*",
"regex": "^\\/proxy-me(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/proxy-me/:path*",
},
{
"destination": "/api/hello",
"regex": "^\\/api-hello(?:\\/)?$",
"source": "/api-hello",
},
{
"destination": "/api/hello?name=:first*",
"regex": "^\\/api-hello-regex(?:\\/(.*))(?:\\/)?$",
"source": "/api-hello-regex/:first(.*)",
},
{
"destination": "/api/hello?hello=:name",
"regex": "^\\/api-hello-param(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/api-hello-param/:name",
},
{
"destination": "/api/dynamic/:name?hello=:name",
"regex": "^\\/api-dynamic-param(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/api-dynamic-param/:name",
},
{
"destination": "/with-params",
"regex": "^(?:\\/([^\\/]+?))\\/post-321(?:\\/)?$",
"source": "/:path/post-321",
},
{
"destination": "/with-params",
"regex": "^\\/unnamed-params\\/nested(?:\\/(.*))(?:\\/([^\\/]+?))(?:\\/(.*))(?:\\/)?$",
"source": "/unnamed-params/nested/(.*)/:test/(.*)",
},
{
"destination": "/with-params",
"regex": "^\\/catchall-rewrite(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/catchall-rewrite/:path*",
},
{
"destination": "/with-params?another=:path*",
"regex": "^\\/catchall-query(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/catchall-query/:path*",
},
{
"destination": "/with-params?myHeader=:myHeader",
"has": [
{
"key": "x-my-header",
"type": "header",
"value": "(?<myHeader>.*)",
},
],
"regex": "^\\/has-rewrite-1(?:\\/)?$",
"source": "/has-rewrite-1",
},
{
"destination": "/with-params?value=:myquery",
"has": [
{
"key": "my-query",
"type": "query",
},
],
"regex": "^\\/has-rewrite-2(?:\\/)?$",
"source": "/has-rewrite-2",
},
{
"destination": "/with-params?authorized=1",
"has": [
{
"key": "loggedIn",
"type": "cookie",
"value": "(?<loggedIn>true)",
},
],
"regex": "^\\/has-rewrite-3(?:\\/)?$",
"source": "/has-rewrite-3",
},
{
"destination": "/with-params?host=1",
"has": [
{
"type": "host",
"value": "example.com",
},
],
"regex": "^\\/has-rewrite-4(?:\\/)?$",
"source": "/has-rewrite-4",
},
{
"destination": "/:hasParam",
"has": [
{
"key": "hasParam",
"type": "query",
},
],
"regex": "^\\/has-rewrite-5(?:\\/)?$",
"source": "/has-rewrite-5",
},
{
"destination": "/with-params",
"has": [
{
"key": "hasParam",
"type": "header",
"value": "with-params",
},
],
"regex": "^\\/has-rewrite-6(?:\\/)?$",
"source": "/has-rewrite-6",
},
{
"destination": "/with-params?idk=:idk",
"has": [
{
"key": "hasParam",
"type": "query",
"value": "(?<idk>with-params|hello)",
},
],
"regex": "^\\/has-rewrite-7(?:\\/)?$",
"source": "/has-rewrite-7",
},
{
"destination": "/blog-catchall/:post",
"has": [
{
"key": "post",
"type": "query",
},
],
"regex": "^\\/has-rewrite-8(?:\\/)?$",
"source": "/has-rewrite-8",
},
{
"destination": "/with-params",
"missing": [
{
"key": "x-my-header",
"type": "header",
"value": "(?<myHeader>.*)",
},
],
"regex": "^\\/missing-rewrite-1(?:\\/)?$",
"source": "/missing-rewrite-1",
},
{
"destination": "/with-params",
"missing": [
{
"key": "my-query",
"type": "query",
},
],
"regex": "^\\/missing-rewrite-2(?:\\/)?$",
"source": "/missing-rewrite-2",
},
{
"destination": "/with-params?authorized=1",
"missing": [
{
"key": "loggedIn",
"type": "cookie",
"value": "(?<loggedIn>true)",
},
],
"regex": "^\\/missing-rewrite-3(?:\\/)?$",
"source": "/missing-rewrite-3",
},
{
"destination": "/hello",
"regex": "^\\/blog\\/about(?:\\/)?$",
"source": "/blog/about",
},
{
"destination": "/overridden",
"regex": "^\\/overridden(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/overridden/:path*",
},
],
"beforeFiles": [
{
"destination": "/with-params?overridden=1",
"has": [
{
"key": "overrideMe",
"type": "query",
},
],
"regex": "^\\/hello(?:\\/)?$",
"source": "/hello",
},
{
"destination": "/blog/:path*",
"regex": "^\\/old-blog(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/old-blog/:path*",
},
{
"destination": "https://example.vercel.sh",
"regex": "^\\/overridden(?:\\/)?$",
"source": "/overridden",
},
{
"destination": "/_sport/nfl/:path*",
"regex": "^\\/nfl(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/nfl/:path*",
},
],
"fallback": [],
},
"rsc": {
"clientParamParsing": true,
"contentTypeHeader": "text/x-component",
"didPostponeHeader": "x-nextjs-postponed",
"dynamicRSCPrerender": true,
"header": "rsc",
"prefetchHeader": "next-router-prefetch",
"prefetchSegmentDirSuffix": ".segments",
"prefetchSegmentHeader": "next-router-segment-prefetch",
"prefetchSegmentSuffix": ".segment.rsc",
"suffix": ".rsc",
"varyHeader": "rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch",
},
"staticRoutes": [
{
"namedRegex": "^/api/hello(?:/)?$",
"page": "/api/hello",
"regex": "^/api/hello(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/auto\\-export/another(?:/)?$",
"page": "/auto-export/another",
"regex": "^/auto\\-export/another(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/docs/v2/more/now\\-for\\-github(?:/)?$",
"page": "/docs/v2/more/now-for-github",
"regex": "^/docs/v2/more/now\\-for\\-github(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/hello(?:/)?$",
"page": "/hello",
"regex": "^/hello(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/hello\\-again(?:/)?$",
"page": "/hello-again",
"regex": "^/hello\\-again(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/multi\\-rewrites(?:/)?$",
"page": "/multi-rewrites",
"regex": "^/multi\\-rewrites(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/nav(?:/)?$",
"page": "/nav",
"regex": "^/nav(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/overridden(?:/)?$",
"page": "/overridden",
"regex": "^/overridden(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/redirect\\-override(?:/)?$",
"page": "/redirect-override",
"regex": "^/redirect\\-override(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/with\\-params(?:/)?$",
"page": "/with-params",
"regex": "^/with\\-params(?:/)?$",
"routeKeys": {},
},
],
"version": 3,
}
`)
} else {
expect(normalizedManifest).toMatchInlineSnapshot(`
{
"appType": "pages",
"basePath": "",
"caseSensitive": true,
"dataRoutes": [
{
"dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/blog\\-catchall\\/(.+?)\\.json$",
"namedDataRouteRegex": "^/_next/data/BUILD_ID/blog\\-catchall/(?<nxtPslug>.+?)\\.json$",
"page": "/blog-catchall/[...slug]",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
{
"dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/overridden\\/([^\\/]+?)\\.json$",
"namedDataRouteRegex": "^/_next/data/BUILD_ID/overridden/(?<nxtPslug>[^/]+?)\\.json$",
"page": "/overridden/[slug]",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
],
"dynamicRoutes": [
{
"namedRegex": "^/_sport/(?<nxtPslug>[^/]+?)(?:/)?$",
"page": "/_sport/[slug]",
"regex": "^\\/_sport\\/([^\\/]+?)(?:\\/)?$",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
{
"namedRegex": "^/_sport/(?<nxtPslug>[^/]+?)/test(?:/)?$",
"page": "/_sport/[slug]/test",
"regex": "^\\/_sport\\/([^\\/]+?)\\/test(?:\\/)?$",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
{
"namedRegex": "^/another/(?<nxtPid>[^/]+?)(?:/)?$",
"page": "/another/[id]",
"regex": "^\\/another\\/([^\\/]+?)(?:\\/)?$",
"routeKeys": {
"nxtPid": "nxtPid",
},
},
{
"namedRegex": "^/api/dynamic/(?<nxtPslug>[^/]+?)(?:/)?$",
"page": "/api/dynamic/[slug]",
"regex": "^\\/api\\/dynamic\\/([^\\/]+?)(?:\\/)?$",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
{
"namedRegex": "^/auto\\-export/(?<nxtPslug>[^/]+?)(?:/)?$",
"page": "/auto-export/[slug]",
"regex": "^\\/auto\\-export\\/([^\\/]+?)(?:\\/)?$",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
{
"namedRegex": "^/blog/(?<nxtPpost>[^/]+?)(?:/)?$",
"page": "/blog/[post]",
"regex": "^\\/blog\\/([^\\/]+?)(?:\\/)?$",
"routeKeys": {
"nxtPpost": "nxtPpost",
},
},
{
"namedRegex": "^/blog\\-catchall/(?<nxtPslug>.+?)(?:/)?$",
"page": "/blog-catchall/[...slug]",
"regex": "^\\/blog\\-catchall\\/(.+?)(?:\\/)?$",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
{
"namedRegex": "^/overridden/(?<nxtPslug>[^/]+?)(?:/)?$",
"page": "/overridden/[slug]",
"regex": "^\\/overridden\\/([^\\/]+?)(?:\\/)?$",
"routeKeys": {
"nxtPslug": "nxtPslug",
},
},
],
"headers": [
{
"headers": [
{
"key": "x-new-header",
"value": "new-value",
},
],
"missing": [
{
"key": "x-my-header",
"type": "header",
"value": "(?<myHeader>.*)",
},
],
"regex": "^\\/missing-headers-1(?:\\/)?$",
"source": "/missing-headers-1",
},
{
"headers": [
{
"key": "x-new-header",
"value": "new-value",
},
],
"missing": [
{
"key": "my-query",
"type": "query",
},
],
"regex": "^\\/missing-headers-2(?:\\/)?$",
"source": "/missing-headers-2",
},
{
"headers": [
{
"key": "x-new-header",
"value": "new-value",
},
],
"missing": [
{
"key": "loggedIn",
"type": "cookie",
"value": "(?<loggedIn>true)",
},
],
"regex": "^\\/missing-headers-3(?:\\/)?$",
"source": "/missing-headers-3",
},
{
"headers": [
{
"key": "x-custom-header",
"value": "hello world",
},
{
"key": "x-another-header",
"value": "hello again",
},
],
"regex": "^\\/add-header(?:\\/)?$",
"source": "/add-header",
},
{
"headers": [
{
"key": "x-first-header",
"value": "first",
},
{
"key": "x-second-header",
"value": "second",
},
],
"regex": "^\\/my-headers(?:\\/(.*))(?:\\/)?$",
"source": "/my-headers/(.*)",
},
{
"headers": [
{
"key": "x-path",
"value": ":path",
},
{
"key": "some:path",
"value": "hi",
},
{
"key": "x-test",
"value": "some:value*",
},
{
"key": "x-test-2",
"value": "value*",
},
{
"key": "x-test-3",
"value": ":value?",
},
{
"key": "x-test-4",
"value": ":value+",
},
{
"key": "x-test-5",
"value": "something https:",
},
{
"key": "x-test-6",
"value": ":hello(world)",
},
{
"key": "x-test-7",
"value": "hello(world)",
},
{
"key": "x-test-8",
"value": "hello{1,}",
},
{
"key": "x-test-9",
"value": ":hello{1,2}",
},
{
"key": "content-security-policy",
"value": "default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com/:path",
},
],
"regex": "^\\/my-other-header(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/my-other-header/:path",
},
{
"headers": [
{
"key": "x-origin",
"value": "https://example.com",
},
],
"regex": "^\\/without-params\\/url(?:\\/)?$",
"source": "/without-params/url",
},
{
"headers": [
{
"key": "x-url",
"value": "https://example.com/:path*",
},
],
"regex": "^\\/with-params\\/url(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/with-params/url/:path*",
},
{
"headers": [
{
"key": "x-url",
"value": "https://example.com:8080?hello=:path*",
},
],
"regex": "^\\/with-params\\/url2(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/with-params/url2/:path*",
},
{
"headers": [
{
"key": "x-something",
"value": "applied-everywhere",
},
],
"regex": "^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/:path*",
},
{
"headers": [
{
"key": "x-something",
"value": "value=:path",
},
{
"key": "path-:path",
"value": "end",
},
],
"regex": "^\\/named-pattern(?:\\/(.*))(?:\\/)?$",
"source": "/named-pattern/:path(.*)",
},
{
"headers": [
{
"key": "x-value",
"value": ":path*",
},
],
"regex": "^\\/catchall-header(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/catchall-header/:path*",
},
{
"has": [
{
"key": "x-my-header",
"type": "header",
"value": "(?<myHeader>.*)",
},
],
"headers": [
{
"key": "x-another",
"value": "header",
},
],
"regex": "^\\/has-header-1(?:\\/)?$",
"source": "/has-header-1",
},
{
"has": [
{
"key": "my-query",
"type": "query",
},
],
"headers": [
{
"key": "x-added",
"value": "value",
},
],
"regex": "^\\/has-header-2(?:\\/)?$",
"source": "/has-header-2",
},
{
"has": [
{
"key": "loggedIn",
"type": "cookie",
"value": "true",
},
],
"headers": [
{
"key": "x-is-user",
"value": "yuuuup",
},
],
"regex": "^\\/has-header-3(?:\\/)?$",
"source": "/has-header-3",
},
{
"has": [
{
"type": "host",
"value": "example.com",
},
],
"headers": [
{
"key": "x-is-host",
"value": "yuuuup",
},
],
"regex": "^\\/has-header-4(?:\\/)?$",
"source": "/has-header-4",
},
],
"onMatchHeaders": [],
"pages404": true,
"redirects": [
{
"destination": "/:path+",
"internal": true,
"priority": true,
"regex": "^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))\\/$",
"source": "/:path+/",
"statusCode": 308,
},
{
"destination": "/with-params",
"missing": [
{
"key": "x-my-header",
"type": "header",
"value": "(?<myHeader>.*)",
},
],
"regex": "^(?!\\/_next)\\/missing-redirect-1(?:\\/)?$",
"source": "/missing-redirect-1",
"statusCode": 307,
},
{
"destination": "/with-params",
"missing": [
{
"key": "my-query",
"type": "query",
},
],
"regex": "^(?!\\/_next)\\/missing-redirect-2(?:\\/)?$",
"source": "/missing-redirect-2",
"statusCode": 307,
},
{
"destination": "/with-params?authorized=1",
"missing": [
{
"key": "loggedIn",
"type": "cookie",
"value": "(?<loggedIn>true)",
},
],
"regex": "^(?!\\/_next)\\/missing-redirect-3(?:\\/)?$",
"source": "/missing-redirect-3",
"statusCode": 307,
},
{
"destination": "/:lang/about",
"regex": "^(?!\\/_next)\\/redirect\\/me\\/to-about(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/redirect/me/to-about/:lang",
"statusCode": 307,
},
{
"destination": "/docs/v2/network/status-codes#:code",
"regex": "^(?!\\/_next)\\/docs\\/router-status(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/docs/router-status/:code",
"statusCode": 301,
},
{
"destination": "/docs/v2/advanced/now-for-github",
"regex": "^(?!\\/_next)\\/docs\\/github(?:\\/)?$",
"source": "/docs/github",
"statusCode": 301,
},
{
"destination": "/docs/v2/more/:all",
"regex": "^(?!\\/_next)\\/docs\\/v2\\/advanced(?:\\/(.*))(?:\\/)?$",
"source": "/docs/v2/advanced/:all(.*)",
"statusCode": 301,
},
{
"destination": "/blog/:id",
"regex": "^(?!\\/_next)\\/hello(?:\\/([^\\/]+?))\\/another(?:\\/)?$",
"source": "/hello/:id/another",
"statusCode": 307,
},
{
"destination": "/",
"regex": "^(?!\\/_next)\\/redirect1(?:\\/)?$",
"source": "/redirect1",
"statusCode": 307,
},
{
"destination": "/",
"regex": "^(?!\\/_next)\\/redirect2(?:\\/)?$",
"source": "/redirect2",
"statusCode": 301,
},
{
"destination": "/another",
"regex": "^(?!\\/_next)\\/redirect3(?:\\/)?$",
"source": "/redirect3",
"statusCode": 302,
},
{
"destination": "/",
"regex": "^(?!\\/_next)\\/redirect4(?:\\/)?$",
"source": "/redirect4",
"statusCode": 308,
},
{
"destination": "/redir-chain2",
"regex": "^(?!\\/_next)\\/redir-chain1(?:\\/)?$",
"source": "/redir-chain1",
"statusCode": 301,
},
{
"destination": "/redir-chain3",
"regex": "^(?!\\/_next)\\/redir-chain2(?:\\/)?$",
"source": "/redir-chain2",
"statusCode": 302,
},
{
"destination": "/",
"regex": "^(?!\\/_next)\\/redir-chain3(?:\\/)?$",
"source": "/redir-chain3",
"statusCode": 303,
},
{
"destination": "https://google.com",
"regex": "^(?!\\/_next)\\/to-external(?:\\/)?$",
"source": "/to-external",
"statusCode": 307,
},
{
"destination": "/with-params?first=:section&second=:name",
"regex": "^(?!\\/_next)\\/query-redirect(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/query-redirect/:section/:name",
"statusCode": 307,
},
{
"destination": "/got-unnamed",
"regex": "^(?!\\/_next)\\/unnamed(?:\\/(first|second))(?:\\/(.*))(?:\\/)?$",
"source": "/unnamed/(first|second)/(.*)",
"statusCode": 307,
},
{
"destination": "/:0",
"regex": "^(?!\\/_next)\\/named-like-unnamed(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/named-like-unnamed/:0",
"statusCode": 307,
},
{
"destination": "/thank-you-next",
"regex": "^(?!\\/_next)\\/redirect-override(?:\\/)?$",
"source": "/redirect-override",
"statusCode": 307,
},
{
"destination": "/:first/:second",
"regex": "^(?!\\/_next)\\/docs(?:\\/(integrations|now-cli))\\/v2(.*)(?:\\/)?$",
"source": "/docs/:first(integrations|now-cli)/v2:second(.*)",
"statusCode": 307,
},
{
"destination": "/somewhere",
"regex": "^(?!\\/_next)\\/catchall-redirect(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/catchall-redirect/:path*",
"statusCode": 307,
},
{
"destination": "https://authserver.example.com/set-password?returnUrl=https%3A%2F%2Fwww.example.com/login",
"regex": "^(?!\\/_next)\\/to-external-with-query(?:\\/)?$",
"source": "/to-external-with-query",
"statusCode": 307,
},
{
"destination": "https://authserver.example.com/set-password?returnUrl=https://www.example.com/login",
"regex": "^(?!\\/_next)\\/to-external-with-query-2(?:\\/)?$",
"source": "/to-external-with-query-2",
"statusCode": 307,
},
{
"destination": "/another?myHeader=:myHeader",
"has": [
{
"key": "x-my-header",
"type": "header",
"value": "(?<myHeader>.*)",
},
],
"regex": "^(?!\\/_next)\\/has-redirect-1(?:\\/)?$",
"source": "/has-redirect-1",
"statusCode": 307,
},
{
"destination": "/another?value=:myquery",
"has": [
{
"key": "my-query",
"type": "query",
},
],
"regex": "^(?!\\/_next)\\/has-redirect-2(?:\\/)?$",
"source": "/has-redirect-2",
"statusCode": 307,
},
{
"destination": "/another?authorized=1",
"has": [
{
"key": "loggedIn",
"type": "cookie",
"value": "true",
},
],
"regex": "^(?!\\/_next)\\/has-redirect-3(?:\\/)?$",
"source": "/has-redirect-3",
"statusCode": 307,
},
{
"destination": "/another?host=1",
"has": [
{
"type": "host",
"value": "example.com",
},
],
"regex": "^(?!\\/_next)\\/has-redirect-4(?:\\/)?$",
"source": "/has-redirect-4",
"statusCode": 307,
},
{
"destination": "/somewhere",
"has": [
{
"key": "x-test-next",
"type": "header",
},
],
"regex": "^(?!\\/_next)(?:\\/([^\\/]+?))\\/has-redirect-5(?:\\/)?$",
"source": "/:path/has-redirect-5",
"statusCode": 307,
},
{
"destination": "https://:subdomain.example.com/some-path/end?a=b",
"has": [
{
"type": "host",
"value": "(?<subdomain>.*)-test.example.com",
},
],
"regex": "^(?!\\/_next)\\/has-redirect-6(?:\\/)?$",
"source": "/has-redirect-6",
"statusCode": 307,
},
{
"destination": "/somewhere?value=:hello",
"has": [
{
"key": "hello",
"type": "query",
"value": "(?<hello>.*)",
},
],
"regex": "^(?!\\/_next)\\/has-redirect-7(?:\\/)?$",
"source": "/has-redirect-7",
"statusCode": 307,
},
],
"rewriteHeaders": {
"pathHeader": "x-nextjs-rewritten-path",
"queryHeader": "x-nextjs-rewritten-query",
},
"rewrites": {
"afterFiles": [
{
"destination": "http://localhost:EXTERNAL_SERVER_PORT/_next/hmr?page=/about",
"regex": "^\\/to-websocket(?:\\/)?$",
"source": "/to-websocket",
},
{
"destination": "/hello",
"regex": "^\\/websocket-to-page(?:\\/)?$",
"source": "/websocket-to-page",
},
{
"destination": "http://localhost:12233",
"regex": "^\\/to-nowhere(?:\\/)?$",
"source": "/to-nowhere",
},
{
"destination": "/auto-export/hello?rewrite=1",
"regex": "^\\/rewriting-to-auto-export(?:\\/)?$",
"source": "/rewriting-to-auto-export",
},
{
"destination": "/auto-export/another?rewrite=1",
"regex": "^\\/rewriting-to-another-auto-export(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/rewriting-to-another-auto-export/:path*",
},
{
"destination": "/another/one",
"regex": "^\\/to-another(?:\\/)?$",
"source": "/to-another",
},
{
"destination": "/404",
"regex": "^\\/nav(?:\\/)?$",
"source": "/nav",
},
{
"destination": "/static/hello.txt",
"regex": "^\\/hello-world(?:\\/)?$",
"source": "/hello-world",
},
{
"destination": "/another",
"regex": "^\\/(?:\\/)?$",
"source": "/",
},
{
"destination": "/multi-rewrites",
"regex": "^\\/another(?:\\/)?$",
"source": "/another",
},
{
"destination": "/hello",
"regex": "^\\/first(?:\\/)?$",
"source": "/first",
},
{
"destination": "/hello-again",
"regex": "^\\/second(?:\\/)?$",
"source": "/second",
},
{
"destination": "/hello",
"regex": "^\\/to-hello(?:\\/)?$",
"source": "/to-hello",
},
{
"destination": "/blog/post-2",
"regex": "^\\/blog\\/post-1(?:\\/)?$",
"source": "/blog/post-1",
},
{
"destination": "/:path",
"regex": "^\\/test(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/test/:path",
},
{
"destination": "/params/this-should-be-the-value",
"regex": "^\\/test-overwrite(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/test-overwrite/:something/:another",
},
{
"destination": "/with-params",
"regex": "^\\/params(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/params/:something",
},
{
"destination": "/with-params?first=:section&second=:name",
"regex": "^\\/query-rewrite(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/query-rewrite/:section/:name",
},
{
"destination": "/_next/:path*",
"regex": "^\\/hidden\\/_next(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/hidden/_next/:path*",
},
{
"destination": "http://localhost:EXTERNAL_SERVER_PORT/:path*",
"regex": "^\\/proxy-me(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/proxy-me/:path*",
},
{
"destination": "/api/hello",
"regex": "^\\/api-hello(?:\\/)?$",
"source": "/api-hello",
},
{
"destination": "/api/hello?name=:first*",
"regex": "^\\/api-hello-regex(?:\\/(.*))(?:\\/)?$",
"source": "/api-hello-regex/:first(.*)",
},
{
"destination": "/api/hello?hello=:name",
"regex": "^\\/api-hello-param(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/api-hello-param/:name",
},
{
"destination": "/api/dynamic/:name?hello=:name",
"regex": "^\\/api-dynamic-param(?:\\/([^\\/]+?))(?:\\/)?$",
"source": "/api-dynamic-param/:name",
},
{
"destination": "/with-params",
"regex": "^(?:\\/([^\\/]+?))\\/post-321(?:\\/)?$",
"source": "/:path/post-321",
},
{
"destination": "/with-params",
"regex": "^\\/unnamed-params\\/nested(?:\\/(.*))(?:\\/([^\\/]+?))(?:\\/(.*))(?:\\/)?$",
"source": "/unnamed-params/nested/(.*)/:test/(.*)",
},
{
"destination": "/with-params",
"regex": "^\\/catchall-rewrite(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/catchall-rewrite/:path*",
},
{
"destination": "/with-params?another=:path*",
"regex": "^\\/catchall-query(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/catchall-query/:path*",
},
{
"destination": "/with-params?myHeader=:myHeader",
"has": [
{
"key": "x-my-header",
"type": "header",
"value": "(?<myHeader>.*)",
},
],
"regex": "^\\/has-rewrite-1(?:\\/)?$",
"source": "/has-rewrite-1",
},
{
"destination": "/with-params?value=:myquery",
"has": [
{
"key": "my-query",
"type": "query",
},
],
"regex": "^\\/has-rewrite-2(?:\\/)?$",
"source": "/has-rewrite-2",
},
{
"destination": "/with-params?authorized=1",
"has": [
{
"key": "loggedIn",
"type": "cookie",
"value": "(?<loggedIn>true)",
},
],
"regex": "^\\/has-rewrite-3(?:\\/)?$",
"source": "/has-rewrite-3",
},
{
"destination": "/with-params?host=1",
"has": [
{
"type": "host",
"value": "example.com",
},
],
"regex": "^\\/has-rewrite-4(?:\\/)?$",
"source": "/has-rewrite-4",
},
{
"destination": "/:hasParam",
"has": [
{
"key": "hasParam",
"type": "query",
},
],
"regex": "^\\/has-rewrite-5(?:\\/)?$",
"source": "/has-rewrite-5",
},
{
"destination": "/with-params",
"has": [
{
"key": "hasParam",
"type": "header",
"value": "with-params",
},
],
"regex": "^\\/has-rewrite-6(?:\\/)?$",
"source": "/has-rewrite-6",
},
{
"destination": "/with-params?idk=:idk",
"has": [
{
"key": "hasParam",
"type": "query",
"value": "(?<idk>with-params|hello)",
},
],
"regex": "^\\/has-rewrite-7(?:\\/)?$",
"source": "/has-rewrite-7",
},
{
"destination": "/blog-catchall/:post",
"has": [
{
"key": "post",
"type": "query",
},
],
"regex": "^\\/has-rewrite-8(?:\\/)?$",
"source": "/has-rewrite-8",
},
{
"destination": "/with-params",
"missing": [
{
"key": "x-my-header",
"type": "header",
"value": "(?<myHeader>.*)",
},
],
"regex": "^\\/missing-rewrite-1(?:\\/)?$",
"source": "/missing-rewrite-1",
},
{
"destination": "/with-params",
"missing": [
{
"key": "my-query",
"type": "query",
},
],
"regex": "^\\/missing-rewrite-2(?:\\/)?$",
"source": "/missing-rewrite-2",
},
{
"destination": "/with-params?authorized=1",
"missing": [
{
"key": "loggedIn",
"type": "cookie",
"value": "(?<loggedIn>true)",
},
],
"regex": "^\\/missing-rewrite-3(?:\\/)?$",
"source": "/missing-rewrite-3",
},
{
"destination": "/hello",
"regex": "^\\/blog\\/about(?:\\/)?$",
"source": "/blog/about",
},
{
"destination": "/overridden",
"regex": "^\\/overridden(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/overridden/:path*",
},
],
"beforeFiles": [
{
"destination": "/with-params?overridden=1",
"has": [
{
"key": "overrideMe",
"type": "query",
},
],
"regex": "^\\/hello(?:\\/)?$",
"source": "/hello",
},
{
"destination": "/blog/:path*",
"regex": "^\\/old-blog(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/old-blog/:path*",
},
{
"destination": "https://example.vercel.sh",
"regex": "^\\/overridden(?:\\/)?$",
"source": "/overridden",
},
{
"destination": "/_sport/nfl/:path*",
"regex": "^\\/nfl(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
"source": "/nfl/:path*",
},
],
"fallback": [],
},
"rsc": {
"clientParamParsing": false,
"contentTypeHeader": "text/x-component",
"didPostponeHeader": "x-nextjs-postponed",
"dynamicRSCPrerender": false,
"header": "rsc",
"prefetchHeader": "next-router-prefetch",
"prefetchSegmentDirSuffix": ".segments",
"prefetchSegmentHeader": "next-router-segment-prefetch",
"prefetchSegmentSuffix": ".segment.rsc",
"suffix": ".rsc",
"varyHeader": "rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch",
},
"staticRoutes": [
{
"namedRegex": "^/api/hello(?:/)?$",
"page": "/api/hello",
"regex": "^/api/hello(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/auto\\-export/another(?:/)?$",
"page": "/auto-export/another",
"regex": "^/auto\\-export/another(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/docs/v2/more/now\\-for\\-github(?:/)?$",
"page": "/docs/v2/more/now-for-github",
"regex": "^/docs/v2/more/now\\-for\\-github(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/hello(?:/)?$",
"page": "/hello",
"regex": "^/hello(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/hello\\-again(?:/)?$",
"page": "/hello-again",
"regex": "^/hello\\-again(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/multi\\-rewrites(?:/)?$",
"page": "/multi-rewrites",
"regex": "^/multi\\-rewrites(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/nav(?:/)?$",
"page": "/nav",
"regex": "^/nav(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/overridden(?:/)?$",
"page": "/overridden",
"regex": "^/overridden(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/redirect\\-override(?:/)?$",
"page": "/redirect-override",
"regex": "^/redirect\\-override(?:/)?$",
"routeKeys": {},
},
{
"namedRegex": "^/with\\-params(?:/)?$",
"page": "/with-params",
"regex": "^/with\\-params(?:/)?$",
"routeKeys": {},
},
],
"version": 3,
}
`)
}
})
it('should have redirects/rewrites in build output with debug flag', async () => {
const manifest = JSON.parse(
await next.readFile('.next/routes-manifest.json')
)
const cleanOutput = stripAnsi(buildCliOutput)
expect(cleanOutput).toContain('Redirects')
expect(cleanOutput).toContain('Rewrites')
expect(cleanOutput).toContain('Headers')
expect(cleanOutput).toMatch(/source.*?/i)
expect(cleanOutput).toMatch(/destination.*?/i)
for (const route of [
...manifest.redirects,
...manifest.rewrites.beforeFiles,
...manifest.rewrites.afterFiles,
...manifest.rewrites.fallback,
]) {
expect(cleanOutput).toContain(route.source)
expect(cleanOutput).toContain(route.destination)
}
for (const route of manifest.headers) {
expect(cleanOutput).toContain(route.source)
for (const header of route.headers) {
expect(cleanOutput).toContain(header.key)
expect(cleanOutput).toContain(header.value)
}
}
})
it('should not show warning for custom routes when not next export', async () => {
expect(next.cliOutput).not.toContain(
`rewrites, redirects, and headers are not applied when exporting your application detected`
)
})
}
})
describe('Custom routes no-op rewrite', () => {
const { next, isTurbopack, isNextStart, skipped } = nextTestSetup({
files: __dirname,
skipStart: true,
env: {
ADD_NOOP_REWRITE: 'true',
},
skipDeployment: true,
})
if (skipped) return
if (isTurbopack && isNextStart) {
it('skipped - not supported in turbopack build mode', () => {})
return
}
let externalServer: http.Server
beforeAll(async () => {
const port = await findPort()
externalServer = http.createServer((req, res) => {
res.end('external')
})
await new Promise<void>((resolve, reject) => {
const onError = (error: Error) => {
reject(error)
}
externalServer.once('error', onError)
externalServer.listen(port, () => {
externalServer.off('error', onError)
resolve()
})
})
await next.patchFile('next.config.js', (content) =>
content.replace(/__EXTERNAL_PORT__/g, String(port))
)
if (!isNextDev) {
await next.build()
}
await next.start()
})
afterAll(() => {
externalServer.close()
})
it('should not error for no-op rewrite and auto export dynamic route', async () => {
const browser = await next.browser('/auto-export/my-slug')
await retry(async () => {
expect(
await browser.eval(() => document.documentElement.innerHTML)
).toMatch(/auto-export.*?my-slug/)
})
})
})
describe('Custom routes solo types', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipStart: true,
disableAutoSkewProtection: true,
skipDeployment: true,
})
if (skipped) return
let externalServer: http.Server
let externalServerPort: number
beforeAll(async () => {
externalServerPort = await findPort()
externalServer = http.createServer((req, res) => {
res.end('external')
})
await new Promise<void>((resolve, reject) => {
const onError = (error: Error) => {
reject(error)
}
externalServer.once('error', onError)
externalServer.listen(externalServerPort, () => {
externalServer.off('error', onError)
resolve()
})
})
await next.patchFile('next.config.js', (content) =>
content.replace(/__EXTERNAL_PORT__/g, String(externalServerPort))
)
})
afterAll(() => {
externalServer.close()
})
it('should work with just headers', async () => {
const content = await next.readFile('next.config.js')
await next.patchFile(
'next.config.js',
content.replace(/(async (?:redirects|rewrites))/g, '$1s')
)
try {
if (!isNextDev) {
await next.build()
}
await next.start()
const res = await next.fetch('/add-header')
const res2 = await next.fetch('/docs/github', { redirect: 'manual' })
const res3 = await next.fetch('/hello-world')
expect(res.headers.get('x-custom-header')).toBe('hello world')
expect(res.headers.get('x-another-header')).toBe('hello again')
expect(res2.status).toBe(404)
expect(res3.status).toBe(404)
} finally {
await next.stop()
await next.patchFile('next.config.js', content)
}
})
it('should work with just rewrites', async () => {
const content = await next.readFile('next.config.js')
await next.patchFile(
'next.config.js',
content.replace(/(async (?:redirects|headers))/g, '$1s')
)
try {
if (!isNextDev) {
await next.build()
}
await next.start()
const res = await next.fetch('/add-header')
const res2 = await next.fetch('/docs/github', { redirect: 'manual' })
const res3 = await next.fetch('/hello-world')
expect(res.headers.get('x-custom-header')).toBeFalsy()
expect(res.headers.get('x-another-header')).toBeFalsy()
expect(res2.status).toBe(404)
expect(res3.status).toBe(200)
expect(await res3.text()).toContain('hello world!')
} finally {
await next.stop()
await next.patchFile('next.config.js', content)
}
})
it('should work with just redirects', async () => {
const content = await next.readFile('next.config.js')
await next.patchFile(
'next.config.js',
content.replace(/(async (?:rewrites|headers))/g, '$1s')
)
try {
if (!isNextDev) {
await next.build()
}
await next.start()
const res = await next.fetch('/add-header')
const res2 = await next.fetch('/docs/github', { redirect: 'manual' })
const res3 = await next.fetch('/hello world')
expect(res.headers.get('x-custom-header')).toBeFalsy()
expect(res.headers.get('x-another-header')).toBeFalsy()
const { pathname } = new URL(res2.headers.get('location'), res.url)
expect(res2.status).toBe(301)
expect(pathname).toBe('/docs/v2/advanced/now-for-github')
expect(res3.status).toBe(404)
} finally {
await next.stop()
await next.patchFile('next.config.js', content)
}
})
})
;(isNextStart ? describe : describe.skip)('Custom routes export', () => {
const { next, isNextDeploy } = nextTestSetup({
files: __dirname,
skipStart: true,
skipDeployment: true,
})
if (isNextDeploy) return
it('should not show warning for custom routes when not next export', async () => {
await next.patchFile('next.config.js', (content) =>
content
.replace(/__EXTERNAL_PORT__/g, '12345')
.replace('// REPLACEME', `output: 'export',`)
)
const { cliOutput } = await next.build()
expect(cliOutput).not.toContain(
`rewrites, redirects, and headers are not applied when exporting your application detected`
)
})
})