Skip to content

Commit 3220f1f

Browse files
fix: use the match result after matching using the matched path header (vercel#77994)
When rendering the page on Vercel, we send the `x-matched-path` header to indicate which route should be rendered. Sometimes, like in the case of a middleware rewrite, we will rewrite a path to a statically generated one. This results in a `x-matched-path` header being equal to the generated page and not the route with the dynamic path params in it. This ensures that if we get such a path that we match against that we do save the result of the path matching as they are valid parameters. This also fixes a related bug associated with route denormalization which must occur before the routes i18n is parsed. <!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: ## For Contributors ### Improving Documentation - Run `pnpm prettier-fix` to fix formatting issues before opening the PR. - Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide ### Adding or Updating Examples - The "examples guidelines" are followed from our contributing doc https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md - Make sure the linting passes by running `pnpm build && pnpm lint`. See https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md ### Fixing a bug - Related issues linked using `fixes #number` - Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ### Adding a feature - Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas) - Related issues/discussions are linked using `fixes #number` - e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) - Documentation added - Telemetry added. In case of a feature if it's used or not. - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ## For Maintainers - Minimal description (aim for explaining to someone not on the team to understand the PR) - When linking to a Slack thread, you might want to share details of the conclusion - Link both the Linear (Fixes NEXT-xxx) and the GitHub issues - Add review comments if necessary to explain to the reviewer the logic behind a change ### What? ### Why? ### How? Closes NEXT- Fixes # --> Fixes vercel#77789 --------- Co-authored-by: Jiwon Choi <devjiwonchoi@gmail.com>
1 parent c94753c commit 3220f1f

File tree

8 files changed

+122
-9
lines changed

8 files changed

+122
-9
lines changed

packages/next/src/server/base-server.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,8 @@ export default abstract class Server<
11311131
matchedPath = this.normalize(matchedPath)
11321132
const normalizedUrlPath = this.stripNextDataPath(urlPathname)
11331133

1134+
matchedPath = denormalizePagePath(matchedPath)
1135+
11341136
// Perform locale detection and normalization.
11351137
const localeAnalysisResult = this.i18nProvider?.analyze(matchedPath, {
11361138
defaultLocale,
@@ -1151,11 +1153,15 @@ export default abstract class Server<
11511153
}
11521154
}
11531155

1154-
// TODO: check if this is needed any more?
1155-
matchedPath = denormalizePagePath(matchedPath)
1156-
11571156
let srcPathname = matchedPath
11581157
let pageIsDynamic = isDynamicRoute(srcPathname)
1158+
let paramsResult: {
1159+
params: ParsedUrlQuery | false
1160+
hasValidParams: boolean
1161+
} = {
1162+
params: false,
1163+
hasValidParams: false,
1164+
}
11591165

11601166
if (!pageIsDynamic) {
11611167
const match = await this.matchers.match(srcPathname, {
@@ -1165,8 +1171,15 @@ export default abstract class Server<
11651171
// Update the source pathname to the matched page's pathname.
11661172
if (match) {
11671173
srcPathname = match.definition.pathname
1168-
// The page is dynamic if the params are defined.
1169-
pageIsDynamic = typeof match.params !== 'undefined'
1174+
1175+
// The page is dynamic if the params are defined. We know at this
1176+
// stage that the matched path is not a static page if the params
1177+
// were parsed from the matched path header.
1178+
if (typeof match.params !== 'undefined') {
1179+
pageIsDynamic = true
1180+
paramsResult.params = match.params
1181+
paramsResult.hasValidParams = true
1182+
}
11701183
}
11711184
}
11721185

@@ -1236,10 +1249,14 @@ export default abstract class Server<
12361249
if (pageIsDynamic) {
12371250
let params: ParsedUrlQuery | false = {}
12381251

1239-
let paramsResult = utils.normalizeDynamicRouteParams(
1240-
queryParams,
1241-
false
1242-
)
1252+
// If we don't already have valid params, try to parse them from
1253+
// the query params.
1254+
if (!paramsResult.hasValidParams) {
1255+
paramsResult = utils.normalizeDynamicRouteParams(
1256+
queryParams,
1257+
false
1258+
)
1259+
}
12431260

12441261
// for prerendered ISR paths we attempt parsing the route
12451262
// params from the URL directly as route-matches may not
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { type Params, expectedParams } from '../../../expected'
2+
3+
export default async function CatchAll({
4+
params,
5+
}: {
6+
params: Promise<Params>
7+
}) {
8+
const received = await params
9+
10+
return <p>{JSON.stringify(received)}</p>
11+
}
12+
13+
export async function generateStaticParams(): Promise<Params[]> {
14+
return [expectedParams]
15+
}
16+
17+
export const dynamic = 'force-dynamic'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Link from 'next/link'
2+
3+
export default function Home() {
4+
return <Link href="/1/2">Go to /1/2</Link>
5+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { ReactNode } from 'react'
2+
export default function Root({ children }: { children: ReactNode }) {
3+
return (
4+
<html>
5+
<body>{children}</body>
6+
</html>
7+
)
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export type Params = {
2+
locale: string
3+
rest: string[]
4+
}
5+
6+
export const expectedParams: Params = {
7+
locale: 'en',
8+
rest: ['1', '2'],
9+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { NextRequest } from 'next/server'
2+
import { NextResponse } from 'next/server'
3+
4+
export function middleware(request: NextRequest) {
5+
return NextResponse.rewrite(
6+
new URL('/en' + request.nextUrl.pathname, request.url)
7+
)
8+
}
9+
10+
export const config = {
11+
matcher: [
12+
/*
13+
* Match all request paths except for the ones starting with:
14+
* - api (API routes)
15+
* - _next/static (static files)
16+
* - _next/image (image optimization files)
17+
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
18+
*/
19+
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
20+
],
21+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* @type {import('next').NextConfig}
3+
*/
4+
const nextConfig = {
5+
experimental: {
6+
ppr: true,
7+
},
8+
}
9+
10+
module.exports = nextConfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
import { expectedParams as expected } from './expected'
3+
4+
describe('ppr-middleware-rewrite-force-dynamic-generate-static-params', () => {
5+
const { next } = nextTestSetup({
6+
files: __dirname,
7+
})
8+
9+
const expectedParams = JSON.stringify(expected)
10+
11+
it('should have correct dynamic params', async () => {
12+
// should be rewritten with /en
13+
const browser = await next.browser('/')
14+
expect(await browser.elementByCss('a').text()).toBe('Go to /1/2')
15+
16+
// navigate to /1/2
17+
await browser.elementByCss('a').click()
18+
19+
// should be rewritten with /en/1/2 with correct params
20+
expect(await browser.elementByCss('p').text()).toBe(expectedParams)
21+
22+
// reloading the page should have the same params
23+
await browser.refresh()
24+
expect(await browser.elementByCss('p').text()).toBe(expectedParams)
25+
})
26+
})

0 commit comments

Comments
 (0)