fix(app-router): scope layout params and layout error boundaries#938
fix(app-router): scope layout params and layout error boundaries#938NathanDrake2406 wants to merge 2 commits intocloudflare:mainfrom
Conversation
Layouts received full route params, layout generateMetadata failures were swallowed, and layout-thrown forbidden()/unauthorized() fell back through the not-found boundary path. Those diverge from Next.js when nested layouts depend on segment params, when head generation fails before page render, or when HTTP access APIs are thrown from a layout. The generated RSC entry now delegates param slicing, head resolution, and parent access-boundary selection to typed runtime helpers. The helpers keep route-specific imports in the entry while moving the behavioral core into unit-tested modules. Tests port Next.js layout params, global-error, forbidden, and unauthorized coverage into focused helper and integration regressions.
commit: |
There was a problem hiding this comment.
Pull request overview
This PR improves App Router Next.js compatibility by scoping layout params to the segment depth (instead of passing full route params everywhere) and by routing layout metadata/access-fallback failures through the correct boundary paths. It also keeps the generated RSC entry slimmer by delegating param slicing, head resolution, and parent access-boundary selection to server helpers.
Changes:
- Add server helpers for segment-scoped params, search param collection, and unified head (metadata/viewport) resolution.
- Route layout
generateMetadata()errors through normal error boundaries; route layoutforbidden()/unauthorized()throws to the correct parent HTTP access boundary. - Update router scanning/entry generation to carry per-layout forbidden/unauthorized boundary arrays and to use the new helpers; add/extend tests and fixtures for Next.js parity.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
packages/vinext/src/server/app-page-params.ts |
New helper to slice matched route params by segment depth for layouts. |
packages/vinext/src/server/app-page-head.ts |
New helper to collect search params and resolve layout+page metadata/viewport with segment-scoped layout params. |
packages/vinext/src/server/app-page-route-wiring.tsx |
Pass segment-scoped params to each layout during element wiring. |
packages/vinext/src/server/app-page-boundary.ts |
Scope params during boundary/layout wrapping; add parent HTTP access boundary selection helper. |
packages/vinext/src/server/app-page-boundary-render.ts |
Use shared head resolution helper for fallback pages; remove duplicated head-resolution logic. |
packages/vinext/src/routing/app-router.ts |
Track forbidden/unauthorized boundary paths per layout level (aligned with layouts). |
packages/vinext/src/entries/app-rsc-entry.ts |
Keep generated entry thin by importing helpers for head resolution, param slicing, and parent access-boundary selection; serialize per-layout boundary arrays. |
packages/vinext/src/shims/metadata.tsx |
Broaden searchParams typing to support repeated query keys (`string |
tests/app-page-params.test.ts |
New unit tests for segment param scoping (including catch-all/optional catch-all behavior). |
tests/app-page-head.test.ts |
New unit tests for search param collection and scoped layout metadata vs full page metadata inputs. |
tests/app-page-route-wiring.test.ts |
Assert layout probing/wiring uses only segment-applicable params. |
tests/app-page-boundary.test.ts |
Update expectations to reflect new segment/param scoping behavior. |
tests/app-router.test.ts |
New integration assertions for layout-thrown forbidden/unauthorized selecting correct boundaries. |
tests/nextjs-compat/global-error.test.ts |
Add coverage for layout generateMetadata() errors flowing to local/global error boundaries. |
tests/fixtures/app-basic/app/nextjs-compat/* |
New fixtures for layout metadata error + layout access fallback scenarios. |
tests/__snapshots__/entry-templates.test.ts.snap |
Snapshot updates reflecting new generated entry imports/behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Layout generateViewport() failures now intentionally share the same boundary path as layout generateMetadata() failures. Copilot flagged that this behavior change needed direct coverage. Add dev and production-preview compat assertions for co-located error.tsx handling and global-error escalation, with fixtures that throw from layout generateViewport().
|
/bigbonk review |
There was a problem hiding this comment.
Solid PR. The refactoring is well-structured: extracted helpers are small, single-purpose, and properly tested. The entry stays thin (matching the repo's architecture guidance), and the boundary arrays for forbidden/unauthorized are properly aligned with the existing not-found pattern.
The behavior changes are well-justified by the Next.js references, and the test coverage is thorough — unit tests for the new helpers, integration tests for both dev and production preview, and the fixtures are minimal and focused.
A few minor observations:
1. Metadata cascade on layout rejection (app-page-head.ts:97-103)
When metadataPromise rejects, the .then() chain on line 97 also rejects. The void accumulatedMetadata.catch(() => null) on line 103 prevents unhandled rejections. This means if layout N throws, layouts N+1..M will also have their parentForLayout reject and their own promises will be rejected too. The Promise.all on line 109 surfaces the first rejection, which is the correct one. The logic is sound.
2. Naming consistency (app-rsc-entry.ts)
forbiddens and unauthorizeds as property names read awkwardly, but consistency with the existing notFounds pattern is more important. If there's ever a rename pass, forbiddenModules/unauthorizedModules would be clearer.
3. Dot exclusion in param parsing (app-page-params.ts:15)
The !segment.includes(".") check correctly prevents matching segments with dots (e.g. route groups like (...)) after catch-all patterns are handled. This matches Next.js behavior where dynamic segments can't contain dots.
4. searchParams type widening (metadata.tsx:357)
The searchParams type change from Record<string, string> to Record<string, string | string[]> is correct — repeated query keys produce arrays in collectAppPageSearchParams, and this was a latent type mismatch.
|
The review has been submitted. Here's a summary of my findings: Approved — this is a well-structured PR that improves Next.js parity in two areas:
The implementation follows the repo's architecture guidelines well — the generated RSC entry stays thin, with real behavior extracted into typed helpers under |
| params: options.makeThenableParams( | ||
| resolveAppPageSegmentParams( | ||
| options.route.routeSegments, | ||
| layoutEntry.treePosition, | ||
| options.matchedParams, | ||
| ), | ||
| ), |
There was a problem hiding this comment.
i think we may also need to align the params passed to the page as well, like we've done for the layout here.
There was a problem hiding this comment.
There was a problem hiding this comment.
What this changes
App Router layouts now receive only the params that apply at their segment depth during both page tree wiring and layout probing. Layout metadata and viewport resolution use the same segment-scoped params, while page metadata still receives full route params and searchParams.
Layout
generateMetadata()andgenerateViewport()failures now propagate to the normal App Router error boundary path instead of being swallowed. Layout-thrownforbidden()andunauthorized()now select the matching parent HTTP access boundary instead of falling through the not-found-only path.The generated RSC entry stays thin: it serializes route imports and delegates param slicing, head resolution, search param collection, and parent access-boundary selection to typed server helpers.
Why
Next.js scopes layout params by walking accumulated parent params per segment, not by passing the full route params object to every layout. Relevant references:
Next.js also routes metadata and HTTP access fallback failures through the relevant error/access boundary paths:
Approach
Add small functional helpers under
server/for segment param slicing, search param collection, head resolution, and parent access-boundary selection. The RSC entry now imports those helpers and keeps the route-specific imperative work in place.The scanner now keeps forbidden and unauthorized boundary arrays aligned with layout levels, matching the existing not-found array shape.
Validation
vp test run tests/app-page-params.test.ts tests/app-page-head.test.ts tests/app-page-boundary.test.ts tests/app-page-route-wiring.test.ts tests/entry-templates.test.ts tests/routing.test.tsvp test run tests/nextjs-compat/global-error.test.tsvp test run tests/app-router.test.ts -t "thrown from a layout uses|layout generateMetadata\(\) does not receive searchParams"vp check tests/app-page-params.test.ts tests/app-page-head.test.ts tests/app-page-boundary.test.ts tests/app-page-route-wiring.test.ts tests/entry-templates.test.ts tests/routing.test.ts tests/nextjs-compat/global-error.test.ts tests/app-router.test.tsvp check tests/nextjs-compat/global-error.test.tsgit diff --checkRisks / follow-ups
Checked open PRs before publishing. Closest adjacent work is #891, #735, and #822; this PR should not overlap their behavior directly.
This PR intentionally does not address remaining App Router opportunities like unknown Server Action IDs, non-action method handling, route-handler
NextResponse.next()validation, or route-handler cookie precedence.