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/giant-bushes-sink.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 13 additions & 9 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -3223,8 +3228,7 @@ export default abstract class Server<
const fallbackRouteParams =
isDynamic &&
isRoutePPREnabled &&
(getRequestMeta(req, 'didSetDefaultRouteMatches') ||
isDebugFallbackShell)
(getRequestMeta(req, 'renderFallbackShell') || isDebugFallbackShell)
? getFallbackRouteParams(pathname)
: null

Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/server/request-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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': '',
},
})

Expand Down