Skip to content

fix(router): honor hybrid pages route priority#1997

Open
NathanDrake2406 wants to merge 25 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/use-params-parity
Open

fix(router): honor hybrid pages route priority#1997
NathanDrake2406 wants to merge 25 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/use-params-parity

Conversation

@NathanDrake2406

@NathanDrake2406 NathanDrake2406 commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Port the upstream App Router useParams behavior across App + Pages fixtures, including the Pages Router case where /pages-dir/foobar must render pages/pages-dir/[dynamic] instead of app/[...path].
  • Add a hybrid route priority helper that compares matched Pages and App dynamic route patterns through the existing Next-compatible route sorter.
  • Export Pages route matchers through the generated bridge so production can decide ownership before render-probing, and align dev middleware ownership with the same ordering.
  • Preserve Vinext static App ownership for identical static App/Page routes, which keeps Cloudflare Worker dev from falling back to the Pages host stub at /.

Root Cause

Vinext kept App and Pages route ownership separate. In production, the App RSC handler only delegated to Pages when no App route matched, so app/[...path] captured /pages-dir/foobar before the Pages Router route could render. Next.js registers Pages route matcher providers before App providers and sorts dynamic route pathnames together, so pages/pages-dir/[dynamic] has priority over an App root catch-all for this request.

The implementation now uses the same sorted dynamic-route comparison only when dynamic route ownership is ambiguous. Static App/Page duplicates remain App-owned in Vinext, matching the existing Cloudflare Worker dev contract for the app-router-cloudflare fixture.

References

Verification

  • vp test run tests/hybrid-route-priority.test.ts tests/app-router-next-config-codegen.test.ts tests/app-pages-bridge.test.ts
  • CI=1 PLAYWRIGHT_PROJECT=use-params-app-pages vp run test:e2e
  • PLAYWRIGHT_PROJECT=cloudflare-dev vp run test:e2e tests/e2e/cloudflare-dev/pages-router.spec.ts tests/e2e/cloudflare-dev/middleware.spec.ts
  • PLAYWRIGHT_PROJECT=app-router vp run test:e2e tests/e2e/app-router/pages-router-use-params.spec.ts
  • vp env exec --node 24 ./scripts/run-nextjs-deploy-suite.sh /Users/nathan/Projects/vinext/.refs/nextjs-v16.2.6 --retries 0 -c 1 --debug test/e2e/app-dir/use-params/use-params.test.ts
  • vp check on the changed runtime, fixture, and test files
  • pre-commit full check, staged unit/integration tests, and knip

@pkg-pr-new

pkg-pr-new Bot commented Jun 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@1997
npm i https://pkg.pr.new/vinext@1997

commit: b251bce

@NathanDrake2406 NathanDrake2406 force-pushed the nathan/use-params-parity branch from 71354f6 to 7be280e Compare June 13, 2026 08:15
NathanDrake2406 added a commit to NathanDrake2406/vinext that referenced this pull request Jun 13, 2026
PR cloudflare#1997 fixed the server-side route ownership for direct document loads,
but the same invariant broke for client-side soft navigations and the
matching prefetch path:

- navigateClientSide() always delegated to the App runtime's RSC fetch,
  even when the Pages route had higher priority. renderPagesFallback()
  short-circuits RSC requests with null, so the App catch-all won.
- prefetchUrl() prefetched an RSC stream for any URL that matched an App
  route, again ignoring Pages ownership.
- The Pages entry was loaded on every hybrid request, even when a static
  App route had already matched (Pages can never win in that case).

Expose the Pages route manifest on the client via a new
__VINEXT_PAGES_LINK_PREFETCH_ROUTES__ window global emitted by both the
App and Pages browser entries. The link shim consults a shared
resolveHybridClientRouteOwner helper that mirrors the server-side
pagesRouteHasPriorityOverAppRoute comparison. When Pages owns the URL,
the click handler issues a window.location navigation and the prefetch
path returns early.

Gate the renderPagesFallback call behind a static-App-route check in
handleAppRscRequest: when a static App route matches, the bridge
cannot win, so skip the eager Pages entry load. Centralise the
comparison in a new resolveHybridRouteOwner helper so server and
client reach the same answer for the same (URL, route pair).

Adds the missing client-navigation coverage to the use-params e2e
fixture (Link from /app/ to /pages-dir/foobar) and unit tests for
the shared owner decision.
@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review June 13, 2026 11:04
PR cloudflare#1997 fixed the server-side route ownership for direct document loads,
but the same invariant broke for client-side soft navigations and the
matching prefetch path:

- navigateClientSide() always delegated to the App runtime's RSC fetch,
  even when the Pages route had higher priority. renderPagesFallback()
  short-circuits RSC requests with null, so the App catch-all won.
- prefetchUrl() prefetched an RSC stream for any URL that matched an App
  route, again ignoring Pages ownership.
- The Pages entry was loaded on every hybrid request, even when a static
  App route had already matched (Pages can never win in that case).

Expose the Pages route manifest on the client via a new
__VINEXT_PAGES_LINK_PREFETCH_ROUTES__ window global emitted by both the
App and Pages browser entries. The link shim consults a shared
resolveHybridClientRouteOwner helper that mirrors the server-side
pagesRouteHasPriorityOverAppRoute comparison. When Pages owns the URL,
the click handler issues a window.location navigation and the prefetch
path returns early.

Gate the renderPagesFallback call behind a static-App-route check in
handleAppRscRequest: when a static App route matches, the bridge
cannot win, so skip the eager Pages entry load. Centralise the
comparison in a new resolveHybridRouteOwner helper so server and
client reach the same answer for the same (URL, route pair).

Adds the missing client-navigation coverage to the use-params e2e
fixture (Link from /app/ to /pages-dir/foobar) and unit tests for
the shared owner decision.
PR review flagged two split-brain bugs in the previous hybrid
ownership fix: a hand-copied client comparator and a Link-only
ownership gate that left programmatic App Router navigations on
the wrong path.

The hand-copied routePrecedence in hybrid-client-route-owner.ts
omitted the static-prefix reduction that lives in
routing/utils.ts#routePrecedence, and used a strict-less-than
comparison that returned App for identical dynamic patterns. The
server returns Pages for both cases (Pages providers sort ahead
of App providers, and routePrecedence subtracts 50 per static
prefix segment). The split produced a real ownership disagreement
on overlapping patterns like /_sites/:slug* (Pages) vs /:slug*
(App).

Add a shared compareHybridRoutePatterns to routing/utils.ts as
the single source of truth: the static/dynamic short-circuits plus
a sortRoutes call (which carries the static-prefix reduction and
the Pages-first equal-pattern tiebreak). The server
pagesRouteHasPriorityOverAppRoute and the client
resolveHybridClientRouteOwner both delegate to it, so they
cannot diverge. Drop the hand-copied routePrecedence entirely.

Wire the ownership check at the App navigation runtime boundary
so useRouter().push, useRouter().replace, gesturePush, and form
submits all get the same hard-nav contract as the Link click
handler. Specifically:

- navigateClientSide: after same-origin normalization, if Pages
  owns the URL, hard-navigate via window.location and return
  (matching the existing external-URL branch).
- _appRouter.prefetch: short-circuit RSC URL construction for
  Pages-owned targets so we do not warm an unusable cache entry.

Centralise the existing two inline hard-nav branches in
navigateClientSide into a hardNavigateTo helper for clarity.

Tests:

- Direct unit tests for compareHybridRoutePatterns covering
  identical-dynamic tiebreak, static-prefix dynamic overlap,
  static-prefix catch-all overlap, and infix-static bonus.
- Direct unit tests for resolveHybridClientRouteOwner mirroring
  the server assertions plus a basePath-stripping test.
- e2e: useRouter().push('/pages-dir/foobar') from an App page
  resolves to the Pages document.
- e2e: useRouter().prefetch('/pages-dir/foobar') issues zero
  RSC requests for the target URL.
The hand-copied comparator note quoted the dynamic-segment scores
(51 / 1000) for the optional-catch-all example (/_sites/:slug*
vs /:slug*). The current optional-catch-all scoring actually
produces 1951 vs 2000. The test assertion was correct; only the
comment was stale.
@NathanDrake2406 NathanDrake2406 force-pushed the nathan/use-params-parity branch from 9559f37 to 6e45654 Compare June 13, 2026 16:29
@NathanDrake2406

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback:

  • should work for single dynamic param (direct-load): Changed navigation URL from /a/b to /a so it actually exercises the [id] route instead of the [id]/[id2] nested route.
  • Added an extra assertion that #param-id2 has count 0 on this page, confirming the single-param route (not the nested one) is serving the request.

Rebased onto latest upstream/main — no conflicts.

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the hybrid route-priority changes. The core feature (Pages dynamic route beating an App root catch-all) is well-covered by tests and the request-pipeline refactor keeps App and Pages rewrite handling in sync. A few things worth addressing before merge — mostly correctness-of-documentation and dead code, plus one comparator behavior question that affects parity claims.

Main concerns:

  1. compareHybridRoutePatterns is documented as delegating to sortRoutes/routePrecedence, but it is actually a separate hand-rolled segment-rank comparator. The two disagree on a large number of dynamic-route pairs (I found ~95 divergences across a small pattern sample, e.g. /:a/x vs /x/:a, /_sites/:a* vs /:a). The segment-rank approach may well be closer to Next.js's real getSortedRoutes, but the comments/test comments asserting it "delegates to sortRoutes" are misleading and will mislead future maintainers about which algorithm is authoritative. Please either (a) actually route the comparison through sortRoutes, or (b) fix the comments to state it intentionally implements Next.js's segment-tree ordering, which differs from vinext's routePrecedence heuristic, and explain why that divergence is safe (it only arbitrates between two already-matched routes, so per-router trie ordering is unaffected).

  2. resolveHybridRouteOwner is exported and tested but has no production caller — the client path uses resolveHybridClientRouteOwner and the server uses pagesRouteHasPriorityOverAppRoute directly. Either wire it in or drop it to avoid dead surface area.

  3. The pages/app rewrite loops changed from "first matching rule, applied once" to "every matching rule applied sequentially (chained)" for beforeFiles/afterFiles/fallback. App and Pages are now consistent with each other, which is good, but please confirm this chaining matches Next.js's resolve-routes behavior — particularly that beforeFiles is meant to chain rather than stop after the first match. This is a behavior change beyond the stated scope of the PR.

Nothing here is a hard blocker on the feature itself; the doc/dead-code items are quick fixes and the chaining question just needs a parity confirmation.

* compareHybridRoutePatterns("/:path+", true, "/dashboard", false) // → "app"
* compareHybridRoutePatterns("/", false, "/", false) // → "app"
*/
export function compareHybridRoutePatterns(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment block and the test comments in tests/hybrid-route-priority.test.ts ("the shared comparator which delegates to sortRoutes") claim this delegates to sortRoutes, but the implementation below is an independent segment-rank comparator. It diverges from routePrecedence/sortRoutes on many dynamic-route pairs (e.g. /:a/x vs /x/:a). Please update the doc to state it intentionally mirrors Next.js's segment-tree ordering (distinct from vinext's routePrecedence heuristic) and note why arbitrating only between two already-matched routes keeps per-router trie ordering unaffected — otherwise a future reader will assume the two stay in lockstep.

* server uses so client navigations, prefetch detection, and direct document
* loads all reach the same answer for the same route pair.
*/
export function resolveHybridRouteOwner<R extends HybridRoutePriorityRoute>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveHybridRouteOwner is exported and unit-tested but has no production caller (the client uses resolveHybridClientRouteOwner, the server uses pagesRouteHasPriorityOverAppRoute). Consider wiring it into the client/server decision points it was designed for, or removing it to avoid dead code that has to be kept in sync with the real comparators.

Comment thread packages/vinext/src/index.ts Outdated
nextConfig?.pageExtensions,
fileMatcher,
);
const appMatch = matchAppRoute(pipelineResult.resolvedUrl, appRoutes);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor inconsistency: line 3927 strips the query with .split("?")[0] before matching Pages, but this passes the full resolvedUrl (with query) to matchAppRoute. It happens to be harmless because matchRouteWithTrie strips the query internally (route-matching.ts:50), but mirroring the .split("?")[0] here would make the two matches visibly consistent.

@@ -407,11 +412,11 @@ export async function runPagesRequest(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes beforeFiles from "first matching rule, applied once" (the old single matchRewrite(..., configRewrites.beforeFiles, ...) call returns on first match) to "apply every matching rule in sequence" (no break). The App handler now does the same, so they're consistent — but please confirm chained beforeFiles rewrites match Next.js resolve-routes semantics. If Next.js stops after the first beforeFiles match, this would over-rewrite.

@ask-bonk

ask-bonk Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review submitted successfully. Here's a summary of what I found in PR #1997.

Review summary

The core feature works and is well-tested. I posted one batch review with 4 inline comments. Findings:

1. Misleading documentation on compareHybridRoutePatterns (utils.ts:119)
The doc comments and test comments claim the comparator "delegates to sortRoutes", but it's actually an independent segment-rank algorithm. I verified ~95 divergences from routePrecedence/sortRoutes across a small pattern sample. The segment-rank approach is likely closer to Next.js's real getSortedRoutes, but the comments are wrong about which algorithm is authoritative — a maintenance trap.

2. Dead code: resolveHybridRouteOwner (hybrid-route-priority.ts:84)
Exported and unit-tested but has no production caller. The client uses resolveHybridClientRouteOwner; the server uses pagesRouteHasPriorityOverAppRoute.

3. Rewrite chaining behavior change (pages-request-pipeline.ts:412)
beforeFiles/afterFiles/fallback changed from "first matching rule, applied once" to "every matching rule applied sequentially." App and Pages are now consistent with each other (good), but this is a scope-creep behavior change that needs a Next.js parity confirmation — especially whether beforeFiles should chain.

4. Minor query-string inconsistency (index.ts:3937)
Pages match strips the query; the App match passes the full URL. Harmless (the trie strips internally) but visually inconsistent.

No hard blockers — items 1, 2, and 4 are quick fixes, and item 3 just needs a parity confirmation against the AGENTS.md-mandated Next.js reference check.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after 9c5dc46 chore(router): clarify hybrid priority semantics. The three main concerns from the prior review look resolved:

  • Misleading docsrouting/utils.ts:100-119 and the test comments in hybrid-route-priority.test.ts no longer claim the comparator "delegates to sortRoutes". They now state it intentionally implements Next.js's segment-tree ordering, distinct from vinext's routePrecedence heuristic, and explain why arbitrating only between two already-matched routes keeps per-router trie ordering unaffected.
  • Dead coderesolveHybridRouteOwner was removed; the remaining exports (pagesRouteHasPriorityOverAppRoute, validateHybridRouteConflicts, compareHybridRoutePatterns) all have production callers.
  • Query-string inconsistency (index.ts dev path) — both matchRoute and matchAppRoute now consume the same query-stripped resolvedPathname.
  • Rewrite chaining — the sequential beforeFiles behavior is now documented (citing Next.js server-utils.ts) and covered by a new test (app-rsc-handler.test.ts:1193 "applies sequential beforeFiles rewrites with accumulated query conditions").

Verified the segment-rank comparator matches Next.js getSortedRoutes on the divergent cases (/:a/x vs /x/:a → app; /_sites/:slug* vs /:slug* → pages). Ran hybrid-route-priority, hybrid-client-route-owner, pages-request-pipeline, and the rewrite-focused app-rsc-handler tests locally — all green (41 + 42 + 21 passing).

One new minor finding (not a blocker), same dead-surface-area category as the previously-removed resolveHybridRouteOwner:

matchesAppRoute (packages/vinext/src/shims/internal/app-route-detection.ts:109) now has no production callers. Both former callers switched to resolveHybridClientRouteOwner in this PR — markAppRouteDetectedOnPrefetch in the same file (line 131: if (resolveHybridClientRouteOwner(href, basePath) !== "app") return;) and performNavigation in router.ts:2249. It's still exported and exercised by tests/pages-router-app-prefetch-detection.test.ts, so it survives knip, but it's now exported-and-tested-but-unused production code — a second route-ownership predicate that has to be kept in sync with the comparator. Consider inlining it as a private helper of the trie match it wraps, or dropping it and folding its assertions into the resolveHybridClientRouteOwner suite.

@ask-bonk

ask-bonk Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review submitted successfully. Here's a summary of what I found in PR #1997.

Review summary

The PR is in good shape. All three of the prior reviewer's main concerns were addressed in commit 9c5dc46:

  1. Misleading docsrouting/utils.ts and the test comments no longer claim the comparator "delegates to sortRoutes". They now correctly document it as an intentional implementation of Next.js's segment-tree ordering, distinct from vinext's routePrecedence heuristic.
  2. Dead coderesolveHybridRouteOwner was removed; remaining exports all have production callers.
  3. Query-string inconsistency — the dev path in index.ts now uses the same query-stripped resolvedPathname for both matchRoute and matchAppRoute.
  4. Rewrite chaining — the sequential beforeFiles behavior is now documented (citing Next.js server-utils.ts) and covered by a new test.

I verified the segment-rank comparator matches Next.js getSortedRoutes on the divergent cases and ran the relevant test files locally — all green (41 + 42 + 21 passing).

One new minor finding (not a blocker): matchesAppRoute (app-route-detection.ts:109) is now dead production code — both former callers switched to resolveHybridClientRouteOwner in this PR. It survives knip only because of its test, but it's a second route-ownership predicate that has to stay in sync with the comparator. This is the same dead-surface-area category as the previously-removed resolveHybridRouteOwner.

No hard blockers.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the hybrid route-priority changes after the latest commits. The implementation is solid and the prior-review concerns are addressed:

  • resolveHybridRouteOwner — removed; all remaining exports (pagesRouteHasPriorityOverAppRoute, validateHybridRouteConflicts, compareHybridRoutePatterns) have production callers.
  • matchesAppRoute — removed entirely from app-route-detection.ts; markAppRouteDetectedOnPrefetch now calls resolveHybridClientRouteOwner.
  • Query-string consistency (index.ts dev path) — both matchRoute and matchAppRoute consume the same query/hash-stripped resolvedPathname (3927-3946).
  • Rewrite chaining — App (app-rsc-handler.ts:598-620) and Pages (pages-request-pipeline.ts:417-433) beforeFiles loops now both apply every matching rule with no break, documented and tested. They are consistent with each other.
  • Doc clarity in routing/utils.tscompareHybridRoutePatterns now correctly documented as an intentional segment-tree implementation distinct from routePrecedence/sortRoutes.

I ran tests/hybrid-route-priority.test.ts and tests/hybrid-client-route-owner.test.ts locally — all 41 green.

One leftover doc issue (not a blocker): the patternFromParts JSDoc in hybrid-client-route-owner.ts still claims the comparator uses sortRoutes. This is the same misleading-documentation category that was fixed in routing/utils.ts but was missed in this file. Quick fix inline.

* `sortRoutes` algorithm can score both Pages and App patterns. The
* `patternParts` array never includes an empty string for the static `/`
* route (the App catch-all handles the bare path), so the simple join is
* safe for everything the route trie actually matches.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale doc reference: this comment claims patternFromParts exists "so the same sortRoutes algorithm can score both Pages and App patterns," but resolveHybridClientRouteOwner (line 206) delegates to compareHybridRoutePatterns, which is the hand-rolled segment-rank comparator — not sortRoutes/routePrecedence. This is the same misleading-documentation issue that was fixed in routing/utils.ts (where the sortRoutes claim was replaced with the segment-tree wording), but it was missed here. A future maintainer reading this will assume sortRoutes is authoritative for hybrid ownership and may try to keep the two in lockstep.

Suggested change
* safe for everything the route trie actually matches.
* route-graph shape (`{ pattern: string }`) so the same
* `compareHybridRoutePatterns` segment-rank comparator can score both Pages
* and App patterns. The

@ask-bonk

ask-bonk Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review submitted successfully. Here's a summary of what I found in PR #1997.

Review summary

The hybrid route-priority feature is well-implemented and well-tested. I verified that all prior-review concerns have been addressed in the latest commits:

  1. resolveHybridRouteOwner dead code — removed; all remaining exports have production callers.
  2. matchesAppRoute dead code — removed entirely from app-route-detection.ts; the caller now uses resolveHybridClientRouteOwner.
  3. Query-string inconsistency (index.ts dev path) — both matchRoute and matchAppRoute now consume the same query/hash-stripped resolvedPathname.
  4. Rewrite chaining — App and Pages beforeFiles loops are now consistent (both apply every matching rule sequentially), documented citing Next.js server-utils.ts, and covered by tests.
  5. Misleading sortRoutes docs in routing/utils.ts — corrected to describe the segment-tree comparator accurately.

I traced the comparator logic against several edge cases (differing lengths, static-prefix dynamics, infix statics) and ran the two new test files locally — all 41 tests pass. I also confirmed both browser entries emit __VINEXT_CLIENT_REWRITES__ and the pages prefetch manifest, so the client/server reach the same ownership decision.

One leftover finding (not a blocker): the patternFromParts JSDoc in hybrid-client-route-owner.ts:90-100 still claims the comparator uses sortRoutes, when it actually delegates to compareHybridRoutePatterns (segment-rank). This is the same misleading-documentation category that was fixed in routing/utils.ts but missed in this file. I posted an inline suggestion with the fix.

No hard blockers.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the hybrid route-priority changes end-to-end. The implementation is solid and I confirmed every prior-review concern is resolved in the current branch state:

  • compareHybridRoutePatterns docs (routing/utils.ts:100-119, hybrid-client-route-owner.ts:90-100) now correctly describe the hand-rolled segment-rank comparator as an intentional Next.js segment-tree implementation, distinct from the routePrecedence/sortRoutes heuristic. The stale sortRoutes claim in patternFromParts JSDoc that the last review flagged has been fixed.
  • Dead code removedresolveHybridRouteOwner and matchesAppRoute are both gone; all remaining exports (pagesRouteHasPriorityOverAppRoute, validateHybridRouteConflicts, compareHybridRoutePatterns, resolveHybridClientRouteOwner) have production callers.
  • Query-string consistency — the dev path in index.ts:3927-3946 strips query/hash into resolvedPathname and feeds it to both matchRoute and matchAppRoute.
  • Rewrite chaining — App (app-rsc-handler.ts:598-620) and Pages (pages-request-pipeline.ts:417-433) beforeFiles loops now both apply every matching rule with no break, and are consistent with each other and documented citing Next.js server-utils.ts.

I traced the comparator against the critical case (pages=/pages-dir/:dynamic vs app=/:path+ → pages owns) and the prior divergence cases (/:a/x vs /x/:a → app; /_sites/:slug* vs /:slug* → pages). Server pattern strings and client patternFromParts output use the same :param/:param+/:param* convention, so the client and server reach the same ownership decision.

No blockers. One minor non-blocking observation below.

}

// Step 9: beforeFiles rewrites
// Next.js server-utils.ts applies every beforeFiles rule in sequence and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking parity note: the comment cites Next.js server-utils.ts for the sequential beforeFiles behavior, but it's worth a quick double-check that Next.js's resolve-routes re-evaluates each beforeFiles rule's has/missing conditions against the post-previous-rewrite URL (which this loop does, since rewriteRequestContext() is re-read each iteration via the updated resolvedUrl/resolvedPathname), rather than against the original incoming request. The App handler at app-rsc-handler.ts:606-610 mirrors this by passing requestContextForResolvedUrl(..., resolvedUrl, ...) each iteration, so the two are consistent — just flagging it so the documented parity claim stays accurate if Next.js's behavior here is ever revisited.

@ask-bonk

ask-bonk Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review submitted successfully. Here's a summary of what I found in PR #1997.

Review summary

The hybrid route-priority feature is well-implemented and well-tested. I reviewed the full implementation and confirmed that every concern from the prior review rounds has been resolved in the current branch state:

  1. Comparator documentation — Both routing/utils.ts and hybrid-client-route-owner.ts (the file the last bonk review flagged) now correctly describe compareHybridRoutePatterns as an intentional segment-rank/segment-tree comparator, distinct from the routePrecedence/sortRoutes heuristic. The stale sortRoutes claim in patternFromParts JSDoc has been fixed.

  2. Dead coderesolveHybridRouteOwner and matchesAppRoute are both removed. All remaining exports have production callers.

  3. Query-string consistencyindex.ts dev path feeds the same query/hash-stripped resolvedPathname to both matchRoute and matchAppRoute.

  4. Rewrite chaining — App and Pages beforeFiles loops are consistent (both apply every matching rule, no break) and documented citing Next.js server-utils.ts.

I traced the comparator against the critical PR case (pages=/pages-dir/:dynamic beats app=/:path+) and the prior divergence cases, and verified server pattern strings and client patternFromParts output use the same param convention so both reach identical ownership decisions.

No blockers. I posted one non-blocking parity note on pages-request-pipeline.ts:414 suggesting a double-check that chained beforeFiles rules re-evaluate has/missing conditions against the post-rewrite URL (the loop already does this via rewriteRequestContext(), and the App handler mirrors it, so they're consistent — just flagging it to keep the documented parity claim accurate).

github run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants