Skip to content

Commit 886f543

Browse files
authored
fix(ppr): fix prerender info matching for rewritten paths (#83359)
## What? Fixed prerender info matching for rewritten paths in App Router by using `resolvedPathname` instead of `parsedUrl.pathname` in the app-page template. ## Why? Previously, the app-page template used `parsedUrl.pathname` to match against `prerenderManifest` routes. However, when middleware rewrites paths, `parsedUrl.pathname` contains the original (unrewritten) path, while `resolvedPathname` contains the actual path that should be used for prerender lookup. This mismatch caused incorrect prerender behavior for routes that were rewritten by middleware, as the handler would look up prerender info using the wrong path. This handling is special cased for when not handling an interception route when cache components is not enabled. This work is being tracked in NAR-335. ## How? - **Modified `packages/next/src/build/templates/app-page.ts`**: - Removed unused `parsedUrl` parameter from handler function - Updated prerender info matching to use `resolvedPathname` instead of `parsedUrl.pathname` - Added explanatory comment about why `resolvedPathname` is the correct choice - **Added comprehensive test suite** in `test/e2e/app-dir/sub-shell-generation-middleware/`: - Tests various middleware rewrite scenarios - Validates sub-shell generation behavior with nested dynamic routes - Tests both static and dynamic prerender scenarios - Includes middleware that rewrites paths to different route structures - **Enhanced deploy testing** in `test/lib/next-modes/next-deploy.ts`: - Added `stderr: 'inherit'` to provide earlier deployment feedback - Improves debugging experience for deployment-related tests The fix ensures that prerender info lookup works correctly for all paths, whether they're rewritten by middleware or not, providing consistent behavior across different routing scenarios. Fixes #83354
1 parent caffa0d commit 886f543

File tree

15 files changed

+372
-3
lines changed

15 files changed

+372
-3
lines changed

packages/next/src/build/templates/app-page.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import * as entryBase from '../../server/app-render/entry-base' with { 'turbopac
8989
import { RedirectStatusCode } from '../../client/components/redirect-status-code'
9090
import { InvariantError } from '../../shared/lib/invariant-error'
9191
import { scheduleOnNextTick } from '../../lib/scheduler'
92+
import { isInterceptionRouteAppPath } from '../../shared/lib/router/utils/interception-routes'
9293

9394
export * from '../../server/app-render/entry-base' with { 'turbopack-transition': 'next-server-utility' }
9495

@@ -150,7 +151,6 @@ export async function handler(
150151
buildId,
151152
query,
152153
params,
153-
parsedUrl,
154154
pageIsDynamic,
155155
buildManifest,
156156
nextFontManifest,
@@ -167,12 +167,24 @@ export async function handler(
167167
interceptionRoutePatterns,
168168
} = prepareResult
169169

170-
const pathname = parsedUrl.pathname || '/'
171170
const normalizedSrcPage = normalizeAppPath(srcPage)
172171

173172
let { isOnDemandRevalidate } = prepareResult
174173

175-
const prerenderInfo = routeModule.match(pathname, prerenderManifest)
174+
// We use the resolvedPathname instead of the parsedUrl.pathname because it
175+
// is not rewritten as resolvedPathname is. This will ensure that the correct
176+
// prerender info is used instead of using the original pathname as the
177+
// source. If however PPR is enabled and cacheComponents is disabled, we
178+
// treat the pathname as dynamic. Currently, there's a bug in the PPR
179+
// implementation that incorrectly leaves %%drp placeholders in the output of
180+
// parallel routes. This is addressed with cacheComponents.
181+
const prerenderInfo =
182+
nextConfig.experimental.ppr &&
183+
!nextConfig.experimental.cacheComponents &&
184+
isInterceptionRouteAppPath(resolvedPathname)
185+
? null
186+
: routeModule.match(resolvedPathname, prerenderManifest)
187+
176188
const isPrerendered = !!prerenderManifest.routes[resolvedPathname]
177189

178190
const userAgent = req.headers['user-agent'] || ''
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Suspense } from 'react'
2+
import SharedComponent from '../../../shared'
3+
4+
export default async function ThirdLayout({
5+
children,
6+
params,
7+
}: {
8+
children: React.ReactNode
9+
params: Promise<Record<string, string>>
10+
}) {
11+
const current = await params
12+
13+
return (
14+
<>
15+
<pre>{JSON.stringify(current)}</pre>
16+
<SharedComponent layout={`/[first]/[second]/[third]`} />
17+
<Suspense>{children}</Suspense>
18+
</>
19+
)
20+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export default async function NotRewritePage({
2+
params,
3+
}: {
4+
params: Promise<{ first: string; second: string; third: string }>
5+
}) {
6+
const { first, second, third } = await params
7+
return (
8+
<div data-slug={`${first}/${second}/${third}`}>
9+
Page /{first}/{second}/{third}
10+
</div>
11+
)
12+
}
13+
14+
export async function generateStaticParams() {
15+
return [{ first: 'first', second: 'second', third: 'third' }]
16+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Suspense } from 'react'
2+
import SharedComponent from '../../shared'
3+
4+
export default async function SecondLayout({
5+
children,
6+
params,
7+
}: {
8+
children: React.ReactNode
9+
params: Promise<Record<string, string>>
10+
}) {
11+
const current = await params
12+
13+
return (
14+
<>
15+
<pre>{JSON.stringify(current)}</pre>
16+
<SharedComponent layout={`/[first]/[second]`} />
17+
<Suspense>{children}</Suspense>
18+
</>
19+
)
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Suspense } from 'react'
2+
import SharedComponent from '../shared'
3+
4+
export default async function FirstLayout({
5+
children,
6+
params,
7+
}: {
8+
children: React.ReactNode
9+
params: Promise<Record<string, string>>
10+
}) {
11+
const current = await params
12+
13+
return (
14+
<>
15+
<pre>{JSON.stringify(current)}</pre>
16+
<SharedComponent layout={`/[first]`} />
17+
<Suspense>{children}</Suspense>
18+
</>
19+
)
20+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { type ReactNode, Suspense } from 'react'
2+
import SharedComponent from './shared'
3+
4+
export default function Root({ children }: { children: ReactNode }) {
5+
return (
6+
<html>
7+
<body>
8+
<SharedComponent layout="/" />
9+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
10+
</body>
11+
</html>
12+
)
13+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>hello world</p>
3+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Suspense } from 'react'
2+
import SharedComponent from '../../shared'
3+
4+
export default async function RewriteSlugLayout({
5+
children,
6+
params,
7+
}: {
8+
params: Promise<Record<string, string>>
9+
children: React.ReactNode
10+
}) {
11+
const current = await params
12+
13+
return (
14+
<>
15+
<pre>{JSON.stringify(current)}</pre>
16+
<SharedComponent layout={`/rewrite/[slug]`} />
17+
<Suspense>{children}</Suspense>
18+
</>
19+
)
20+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export default async function RewritePage({
2+
params,
3+
}: {
4+
params: Promise<{ slug: string }>
5+
}) {
6+
const { slug } = await params
7+
return <div data-rewrite-slug={slug}>Page /rewrite/{slug}</div>
8+
}
9+
10+
export async function generateStaticParams() {
11+
return []
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Suspense } from 'react'
2+
import SharedComponent from '../shared'
3+
4+
export default async function RewriteLayout({
5+
children,
6+
params,
7+
}: {
8+
params: Promise<Record<string, string>>
9+
children: React.ReactNode
10+
}) {
11+
const current = await params
12+
13+
return (
14+
<div>
15+
<pre>{JSON.stringify(current)}</pre>
16+
<SharedComponent layout={`/rewrite`} />
17+
<Suspense>{children}</Suspense>
18+
</div>
19+
)
20+
}

0 commit comments

Comments
 (0)