Skip to content

Commit e0d4df2

Browse files
[ppr] Narrow condition for fallback shell generation at runtime (#79565)
This addresses a bug where a fallback shell was generated at runtime when it was not expected to do so. This was an issue with how the `x-now-route-matches` header was parsed and was incorrectly assuming that a falsy header was the same as comparing it to a string. This correctly only generates the fallback shell if the header is present and was an empty string as was sent by Vercel. Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
1 parent 3175a22 commit e0d4df2

File tree

5 files changed

+38
-11
lines changed

5 files changed

+38
-11
lines changed

.changeset/giant-bushes-sink.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'next': patch
3+
---
4+
5+
Resolved bug where hitting the parameterized path directly would cause a fallback shell generation instead of just rendering the route with the parameterized placeholders.

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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,19 +1346,24 @@ export default abstract class Server<
13461346
}
13471347
}
13481348

1349-
// handle the actual dynamic route name being requested
1349+
// If the pathname being requested is the same as the source
1350+
// pathname, and we don't have valid params, we want to use the
1351+
// default route matches.
13501352
if (
13511353
utils.defaultRouteMatches &&
13521354
normalizedUrlPath === srcPathname &&
1353-
!paramsResult.hasValidParams &&
1354-
!utils.normalizeDynamicRouteParams({ ...params }, true)
1355-
.hasValidParams
1355+
!paramsResult.hasValidParams
13561356
) {
13571357
params = utils.defaultRouteMatches
13581358

1359-
// Mark that the default route matches were set on the request
1360-
// during routing.
1361-
addRequestMeta(req, 'didSetDefaultRouteMatches', true)
1359+
// If the route matches header is an empty string, we want to
1360+
// render a fallback shell. This is because we know this came from
1361+
// a prerender (it has the header) but it's values were filtered
1362+
// out (because the allowQuery was empty). If it was undefined
1363+
// then we know that the request is hitting the lambda directly.
1364+
if (routeMatchesHeader === '') {
1365+
addRequestMeta(req, 'renderFallbackShell', true)
1366+
}
13621367
}
13631368

13641369
if (params) {
@@ -3223,8 +3228,7 @@ export default abstract class Server<
32233228
const fallbackRouteParams =
32243229
isDynamic &&
32253230
isRoutePPREnabled &&
3226-
(getRequestMeta(req, 'didSetDefaultRouteMatches') ||
3227-
isDebugFallbackShell)
3231+
(getRequestMeta(req, 'renderFallbackShell') || isDebugFallbackShell)
32283232
? getFallbackRouteParams(pathname)
32293233
: null
32303234

packages/next/src/server/request-meta.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ export interface RequestMeta {
153153
middlewareInvoke?: boolean
154154

155155
/**
156-
* Whether the default route matches were set on the request during routing.
156+
* Whether the request should render the fallback shell or not.
157157
*/
158-
didSetDefaultRouteMatches?: boolean
158+
renderFallbackShell?: boolean
159159

160160
/**
161161
* Whether the request is for the custom error page.

test/e2e/app-dir/empty-fallback-shells/empty-fallback-shells.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,23 @@ describe('empty-fallback-shells', () => {
118118
expect(res.headers.get('x-nextjs-postponed')).not.toBe('1')
119119
}
120120
})
121+
122+
it('does not render a fallback shell when using a params placeholder', async () => {
123+
// This should trigger a blocking prerender of the route shell.
124+
const res = await next.fetch(
125+
'/with-cached-io/without-suspense/params-in-page/[slug]'
126+
)
127+
128+
expect(res.status).toBe(200)
129+
130+
const html = await res.text()
131+
132+
// This should render the encoded param in the route shell, and not
133+
// interpret the param as a fallback param, and subsequently try to
134+
// render the fallback shell instead, which would fail because of the
135+
// missing parent suspense boundary.
136+
expect(html).toContain('page-%5Bslug%5D')
137+
})
121138
})
122139

123140
describe('and the params accessed in a cached non-page function', () => {

test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ describe('required server files app router', () => {
355355
'x-matched-path': '/postpone/isr/[slug]',
356356
// We don't include the `x-now-route-matches` header because we want to
357357
// test that the fallback route params are correctly set.
358+
'x-now-route-matches': '',
358359
},
359360
})
360361

0 commit comments

Comments
 (0)