Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/shaggy-owls-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'next': patch
---

Fixed rewrite params of the interception routes not being parsed correctly when deployed to Vercel
Comment thread
devjiwonchoi marked this conversation as resolved.
Outdated
42 changes: 41 additions & 1 deletion packages/next/src/server/server-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import { decodeQueryPathParameter } from './lib/decode-query-path-parameter'
import type { DeepReadonly } from '../shared/lib/deep-readonly'
import { parseReqUrl } from '../lib/url'
import { formatUrl } from '../shared/lib/router/utils/format-url'
import { parseAndValidateFlightRouterState } from './app-render/parse-and-validate-flight-router-state'
import { isInterceptionRouteRewrite } from '../lib/generate-interception-routes-rewrites'
import { NEXT_ROUTER_STATE_TREE_HEADER } from '../client/components/app-router-headers'
import { getSelectedParams } from '../client/components/router-reducer/compute-changed-path'

export function normalizeCdnUrl(
req: BaseNextRequest | IncomingMessage,
Expand Down Expand Up @@ -209,7 +213,7 @@ export function getServerUtils({
req: BaseNextRequest | IncomingMessage,
parsedUrl: UrlWithParsedQuery
) {
const rewriteParams = {}
const rewriteParams: Record<string, string> = {}
let fsPathname = parsedUrl.pathname

const matchesPage = () => {
Expand Down Expand Up @@ -250,6 +254,28 @@ export function getServerUtils({
}

if (params) {
try {
// An interception rewrite might reference a dynamic param for a route the user
// is currently on, which wouldn't be extractable from the matched route params.
// This attempts to extract the dynamic params from the provided router state.
if (isInterceptionRouteRewrite(rewrite as Rewrite)) {
const stateHeader =
req.headers[NEXT_ROUTER_STATE_TREE_HEADER.toLowerCase()]

if (stateHeader) {
params = {
...getSelectedParams(
parseAndValidateFlightRouterState(stateHeader)
),
...params,
}
}
}
} catch (err) {
// this is a no-op -- we couldn't extract dynamic params from the provided router state,
// so we'll just use the params from the route matcher
}

const { parsedDestination, destQuery } = prepareDestination({
appendParamsToQuery: true,
destination: rewrite.destination,
Expand All @@ -266,6 +292,20 @@ export function getServerUtils({
Object.assign(parsedUrl.query, parsedDestination.query)
delete (parsedDestination as any).query

// for each property in parsedUrl.query, if the value is parametrized (eg :foo), look up the value
// in rewriteParams and replace the parametrized value with the actual value
// this is used when the rewrite destination does not contain the original source param
// and so the value is still parametrized and needs to be replaced with the actual rewrite param
Object.entries(parsedUrl.query).forEach(([key, value]) => {
if (value && typeof value === 'string' && value.startsWith(':')) {
const paramName = value.slice(1)
const actualValue = rewriteParams[paramName]
if (actualValue) {
parsedUrl.query[key] = actualValue
}
}
})

Object.assign(parsedUrl, parsedDestination)

fsPathname = parsedUrl.pathname
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Home() {
return <p>intercepted!</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Default() {
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function Layout(props: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<>
{props.children}
{props.modal}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Link from 'next/link'

export default async function Page(props: {
params: Promise<{ foo_id: string; bar_id: string }>
}) {
const params = await props.params
return (
<>
<h1>
foo id {params.foo_id}, bar id {params.bar_id}
</h1>
<Link href="/baz_id/1">Link to bug report 1</Link>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default async function Home({
params,
}: {
params: Promise<{ baz_id: string }>
}) {
const { baz_id } = await params
return <p>baz_id/{baz_id}</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Link from 'next/link'

export default function Home() {
return <Link href="/1/1">Start from /1/1</Link>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}

module.exports = nextConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { nextTestSetup } from 'e2e-utils'

describe('parallel-routes-and-interception-nested-dynamic-routes', () => {
const { next } = nextTestSetup({
files: __dirname,
})

it('should intercept the route for nested dynamic routes', async () => {
const browser = await next.browser('/1/1')
expect(await browser.elementByCss('h1').text()).toBe('foo id 1, bar id 1')
await browser.elementByCss('a').click()

// Should intercept the route.
expect(await browser.waitForElementByCss('p').text()).toBe('intercepted!')
// Should preserve the previous component.
expect(await browser.elementByCss('h1').text()).toBe('foo id 1, bar id 1')

await browser.refresh()
// Should display the correct /baz_id/1 content.
expect(await browser.waitForElementByCss('p').text()).toBe('baz_id/1')
})
})