From 4dc54e86afac498815f197e781e4b15102699e22 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Thu, 22 May 2025 19:47:59 +0200 Subject: [PATCH 1/4] Add failing test for incorrect fallback shell revalidation --- .../empty-fallback-shells.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/e2e/app-dir/empty-fallback-shells/empty-fallback-shells.test.ts b/test/e2e/app-dir/empty-fallback-shells/empty-fallback-shells.test.ts index c72980fb040d..cfc4ce7ece8b 100644 --- a/test/e2e/app-dir/empty-fallback-shells/empty-fallback-shells.test.ts +++ b/test/e2e/app-dir/empty-fallback-shells/empty-fallback-shells.test.ts @@ -118,6 +118,23 @@ describe('empty-fallback-shells', () => { expect(res.headers.get('x-nextjs-postponed')).not.toBe('1') } }) + + it('does not render a fallback shell when using a params placeholder', async () => { + // This should trigger a blocking prerender of the route shell. + const res = await next.fetch( + '/with-cached-io/without-suspense/params-in-page/[slug]' + ) + + expect(res.status).toBe(200) + + const html = await res.text() + + // This should render the encoded param in the route shell, and not + // interpret the param as a fallback param, and subsequently try to + // render the fallback shell instead, which would fail because of the + // missing parent suspense boundary. + expect(html).toContain('page-%5Bslug%5D') + }) }) describe('and the params accessed in a cached non-page function', () => { From 84a67371aeb65e8e342ecae498ce74a0d3ff6b10 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 23 May 2025 12:45:56 -0600 Subject: [PATCH 2/4] fix: adjust logic for when we trigger a fallback shell render --- packages/next/src/server/base-server.ts | 22 +++++++++++++--------- packages/next/src/server/request-meta.ts | 4 ++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 825749fbb024..9f7be56c1c01 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -1346,19 +1346,24 @@ export default abstract class Server< } } - // handle the actual dynamic route name being requested + // If the pathname being requested is the same as the source + // pathname, and we don't have valid params, we want to use the + // default route matches. if ( utils.defaultRouteMatches && normalizedUrlPath === srcPathname && - !paramsResult.hasValidParams && - !utils.normalizeDynamicRouteParams({ ...params }, true) - .hasValidParams + !paramsResult.hasValidParams ) { params = utils.defaultRouteMatches - // Mark that the default route matches were set on the request - // during routing. - addRequestMeta(req, 'didSetDefaultRouteMatches', true) + // If the route matches header is an empty string, we want to + // render a fallback shell. This is because we know this came from + // a prerender (it has the header) but it's values were filtered + // out (because the allowQuery was empty). If it was undefined + // then we know that the request is hitting the lambda directly. + if (routeMatchesHeader === '') { + addRequestMeta(req, 'renderFallbackShell', true) + } } if (params) { @@ -3223,8 +3228,7 @@ export default abstract class Server< const fallbackRouteParams = isDynamic && isRoutePPREnabled && - (getRequestMeta(req, 'didSetDefaultRouteMatches') || - isDebugFallbackShell) + (getRequestMeta(req, 'renderFallbackShell') || isDebugFallbackShell) ? getFallbackRouteParams(pathname) : null diff --git a/packages/next/src/server/request-meta.ts b/packages/next/src/server/request-meta.ts index 50d6f3852c03..9f116ad6dbe2 100644 --- a/packages/next/src/server/request-meta.ts +++ b/packages/next/src/server/request-meta.ts @@ -153,9 +153,9 @@ export interface RequestMeta { middlewareInvoke?: boolean /** - * Whether the default route matches were set on the request during routing. + * Whether the request should render the fallback shell or not. */ - didSetDefaultRouteMatches?: boolean + renderFallbackShell?: boolean /** * Whether the request is for the custom error page. From f42eae4bec3c5e037e4b56192680fdb5f09012bc Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 23 May 2025 13:16:08 -0600 Subject: [PATCH 3/4] chore: added changeset --- .changeset/giant-bushes-sink.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/giant-bushes-sink.md diff --git a/.changeset/giant-bushes-sink.md b/.changeset/giant-bushes-sink.md new file mode 100644 index 000000000000..12493bb3b955 --- /dev/null +++ b/.changeset/giant-bushes-sink.md @@ -0,0 +1,5 @@ +--- +'next': patch +--- + +Resolved bug where hitting the parameterized path directly would cause a fallback shell generation instead of just rendering the route with the parameterized placeholders. From 3d6cc9b2d520627d3aae0e7988d380a799c01d6e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 23 May 2025 13:31:06 -0600 Subject: [PATCH 4/4] test: fixed test condition to match production environment --- .../required-server-files/required-server-files-ppr.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts b/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts index 0438ac786935..bf976bf76b0c 100644 --- a/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts +++ b/test/production/standalone-mode/required-server-files/required-server-files-ppr.test.ts @@ -355,6 +355,7 @@ describe('required server files app router', () => { 'x-matched-path': '/postpone/isr/[slug]', // We don't include the `x-now-route-matches` header because we want to // test that the fallback route params are correctly set. + 'x-now-route-matches': '', }, })