Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 in certain deployed environments
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')
})
})