Files
Andrew Clark 16eab3ac5d [backport] Fix: Optimistic routing bugs leading to repeated prefetch loops (#97325)
Backports #97128 ("Fix: Optimistic routing bugs leading to repeated
prefetch loops") to `next-16-3`.

Fixes an infinite prefetch loop in optimistic routing. A proxy that
rewrites every URL to inject a leading path segment — i18n with the
default locale hidden from the URL, so `/one/two` is served as
`/en/one/two` — combined with a fully dynamic target route like
`/[locale]/[...pages]`, produces a prefetch that never settles. When a
prefetch response came back, the client never checked whether the
rendered tree matched the one it had requested. When the params baked
into the request were wrong because of the rewrite, the task kept
re-attempting to fulfill the missing data and looped forever.

The prefetch path now records a dynamic-rewrite marker on the local
route definition when the response's tree diverges from the requested
one, which disables further optimistic resolution of that route and
invalidates the entries derived from it. This is the same strategy
normal navigation responses already used, now applied to the prefetch
path.

A second, related bug is also fixed: parallel routes with conflicting
dynamic params at the same level (`@modal/[...catchAll]` next to
`[username]`) can't be distinguished by the traversal algorithm, which
assumes each segment resolves independently without inspecting sibling
branches. When that scenario is detected, optimistic routing is now
disabled for the conflicting route rather than binding one branch's URL
parts to another branch's params.

### Verification on this branch

Cherry-picked from `5942b37a42`. Three source files conflicted, all
because `next-16-3` predates the post-16.3.0 refactor that split the
client's response decoder into its own module:

- `segment-cache/decode-server-response.ts` — this file does not exist
on this branch; it was extracted out of `navigation.ts` after 16.3.0.
Its change (tracking whether the decoded tree's segment identities
diverge from the base request tree) was ported into `navigation.ts`,
where the equivalent logic still lives. Canary decodes a compact
transport tree and compares segment identities inline during the walk;
this branch instead merges the response patch onto the base
`FlightRouterState` before converting it to a `RouteTree`. The check is
therefore re-expressed as a read-only walk in
`convertServerPatchToFullTree` that compares each server patch against
the base tree it is applied to, following canary's descent
(`convertServerPatchToFullTreeImpl`) exactly and using the same
comparison rules: segment identity via `matchSegment`, page segments
matched modulo embedded search params, server-filled `__DEFAULT__`
segments ignored, and inactive parallel-route branches (those carrying a
refresh state in the base) skipped. The result is threaded out through
`NavigationSeed.treeDivergedFromBase`.
- `segment-cache/cache.ts` — the consuming block in
`fetchSegmentPrefetchesUsingDynamicRequest` (mark-as-dynamic-rewrite,
invalidate derived entries, reject with immediate expiry) is
byte-identical to canary. The surrounding conflicts were only
incidental: the `RouteTreeAccumulator` type gaining
`treeDivergedFromBase`, and canary's transport-decoder call vs. this
branch's `convertRootFlightRouterStateToRouteTree`. Resolved by keeping
this branch's calls and adding the new field to the accumulators.
Canary's inline `NavigationSeed` construction in
`writeDynamicTreeResponseIntoCache` does not apply here because that
function delegates to `convertServerPatchToFullTree`, which now carries
the field.
- `router-reducer/create-initial-router-state.ts` — a one-line
accumulator initializer; kept this branch's
`convertRootFlightRouterStateToRouteTree` call and added
`treeDivergedFromBase: false`.

The other touched source files applied cleanly and are byte-identical to
canary: `segment-cache/optimistic-routes.ts` (the parallel-route
conflict detection) and `route-params.ts` (exporting
`canonicalizeURLPart`), along with `ppr-navigations.ts`, the small
cleanly-applied hunk in `navigation.ts`, and every test fixture.

`tsc` passes on the `next` package for all files touched here.
Behavioral verification is left to CI on this branch; the change carries
the `proxy-prefix-rewrite-prefetch-loop` and
`modal-catchall-sibling-dynamic-prefetch-loop` end-to-end suites plus
additions to the `optimistic-routing` suite.

Opened as a draft because the `decode-server-response.ts` →
`navigation.ts` port is a manual re-implementation across the refactor
boundary rather than a mechanical cherry-pick. The divergence walk in
`convertServerPatchToFullTree` is the part most worth a close review.

<!-- NEXT_JS_LLM -->
2026-08-13 17:17:46 -04:00
..
…
…