Upstream change
Next.js 5942b37 — "Fix: Optimistic routing bugs leading to repeated prefetch loops" (#97128). Fixes #97135.
The bugs
Two root causes in the client segment-cache optimistic routing (packages/next/src/client/components/segment-cache/):
-
Prefix-rewrite prefetch loop. A proxy that rewrites every URL to inject a leading path segment (e.g. i18n with default locale hidden: /one/two → /en/one/two) combined with a fully dynamic target route like /[locale]/[...pages] causes an infinite prefetch loop that never resolves. When receiving a prefetch response, Next.js did not verify the response matched the expected result; a tree mismatch made the prefetch task repeatedly retry to fulfill missing data.
-
Conflicting dynamic parallel-route siblings. 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 siblings/children. This also triggers a loop.
The fix
- Record on the local route definition that a dynamic rewrite occurred, disabling further optimistic resolution attempts for that route — the same strategy already used for normal navigation responses, now applied to the prefetch path (
handleMismatchDueToRewrite).
- Compare the param's cache key (parsed from the rendered pathname) against the URL parts the segment would consume, per param type:
d (single dynamic) canonicalizes the URL part and compares; c/oc (catch-all/optional catch-all) joins remaining parts with / and compares; interception params are skipped. On mismatch, bail to server resolution.
- Add
hasConflictingDynamicChildren to the known-route-part trie: when parallel branches disagree about the dynamic segment (different param name/type) at a level, mark it conflicted so discovery stops storing patterns beneath it and matching bails to server resolution.
- New
canonicalizeURLPart helper in route-params.ts.
Why it's relevant to vinext
vinext reimplements App Router client-side navigation, prefetching, and the segment cache. Combined with vinext's middleware/rewrite handling (prefix rewrites for i18n) and parallel routes, the same infinite prefetch loops can occur. This is a high-impact correctness/perf bug (request waterfall / loop that never resolves), originally reported as a next-intl prefetch waterfall.
Action
- Audit vinext's optimistic routing / segment-cache prediction for rewrite-affected responses (prefix-injecting rewrites) and add a mismatch bail-out that disables optimistic resolution for the affected route.
- Add cache-key vs URL-part comparison per dynamic param type (single, catch-all, optional catch-all) before predicting a route.
- Handle conflicting dynamic parallel-route siblings by bailing out to server resolution.
- Port the two e2e fixtures:
proxy-prefix-rewrite-prefetch-loop and modal-catchall-sibling-dynamic-prefetch-loop, plus the added optimistic-routing cases.
References
Upstream change
Next.js 5942b37 — "Fix: Optimistic routing bugs leading to repeated prefetch loops" (#97128). Fixes #97135.
The bugs
Two root causes in the client segment-cache optimistic routing (
packages/next/src/client/components/segment-cache/):Prefix-rewrite prefetch loop. A proxy that rewrites every URL to inject a leading path segment (e.g. i18n with default locale hidden:
/one/two→/en/one/two) combined with a fully dynamic target route like/[locale]/[...pages]causes an infinite prefetch loop that never resolves. When receiving a prefetch response, Next.js did not verify the response matched the expected result; a tree mismatch made the prefetch task repeatedly retry to fulfill missing data.Conflicting dynamic parallel-route siblings. 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 siblings/children. This also triggers a loop.The fix
handleMismatchDueToRewrite).d(single dynamic) canonicalizes the URL part and compares;c/oc(catch-all/optional catch-all) joins remaining parts with/and compares; interception params are skipped. On mismatch, bail to server resolution.hasConflictingDynamicChildrento the known-route-part trie: when parallel branches disagree about the dynamic segment (different param name/type) at a level, mark it conflicted so discovery stops storing patterns beneath it and matching bails to server resolution.canonicalizeURLParthelper inroute-params.ts.Why it's relevant to vinext
vinext reimplements App Router client-side navigation, prefetching, and the segment cache. Combined with vinext's middleware/rewrite handling (prefix rewrites for i18n) and parallel routes, the same infinite prefetch loops can occur. This is a high-impact correctness/perf bug (request waterfall / loop that never resolves), originally reported as a next-intl prefetch waterfall.
Action
proxy-prefix-rewrite-prefetch-loopandmodal-catchall-sibling-dynamic-prefetch-loop, plus the addedoptimistic-routingcases.References