Skip to content

fix(app-router): recover mismatched prefetch rewrites - #2453

Open
james-elicx wants to merge 8 commits into
mainfrom
codex/fix-mismatching-prefetch-28478866791
Open

fix(app-router): recover mismatched prefetch rewrites#2453
james-elicx wants to merge 8 commits into
mainfrom
codex/fix-mismatching-prefetch-28478866791

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • recover App Router navigations when a prefetched URL is rewritten to a different route
  • preserve optimistic loading shells by probing nested server-component Suspense fallbacks
  • only demote query-string prefetch cache reuse when middleware matcher conditions confidently require skipping prefetch requests

Validation

  • vp check packages/vinext/src/shims/link.tsx tests/link-navigation.test.ts packages/vinext/src/server/app-optimistic-routing.ts packages/vinext/src/server/app-page-route-wiring.tsx
  • vp test run tests/link-navigation.test.ts tests/app-page-probe.test.ts tests/app-page-dispatch.test.ts tests/app-optimistic-routing.test.ts tests/app-page-route-wiring.test.ts
  • vp exec playwright test -c tests/e2e/app-router/nextjs-compat/playwright.nextjs-compat.config.ts tests/e2e/app-router/nextjs-compat/mismatching-prefetch.browser.spec.ts --project=chrome-browser-specific

Independent review loop completed with no findings after follow-up fixes.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Jul 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 18b4855

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@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.

Review: fix(app-router): recover mismatched prefetch rewrites

Reviewed all 18 changed files, ran vp check on the touched source files (clean) and the five relevant unit suites (210 tests passing). This is a solid, well-tested change that stays close to Next.js behavior. No blocking issues found.

What works well

  • Matcher parity fix is correct. middleware-runtime.ts now evaluates has/missing conditions against an unmodified matcherRequest, plumbed from app-middleware.ts as options.request (original) while the user middleware still receives the Flight-stripped middlewareRequest. This matches Next.js, which evaluates matcher conditions before stripping router headers. The options.matcherRequest ?? options.request fallback preserves existing behavior for Pages Router and other callers that don't set it. Backed by a ported test in shims.test.ts linking to the Next.js source.
  • Conservative demotion. automaticSearchPrefetchMayMismatchNavigation only demotes cache reuse for mode === "auto" query-string prefetches, and clientMiddlewareSourceMatches fails closed (returns false) on regex/group sources, which is the right direction here (only demote when confident). Note this deliberately diverges from the Pages Router copy in pages-data-target.ts:81, which fails open — the opposite default is correct because the two functions answer opposite questions.
  • Probe safety. probePrefetchLoadingShellFallback wraps the fallback probe in runWithConnectionProbe + runWithSuppressedHookWarning + try/catch, and extractFirstSuspenseFallback stops at the first Suspense fallback without descending into connection()-suspending children (verified by the new app-page-probe.test.ts case). The recursion depth guards (> 100) are present in both the extract and the client-side createSuspendingElementInsideFirstSuspense.

Non-blocking observations

  1. Scheduling change is broader than the PR title. prefetchUrl now dispatches App Router prefetches immediately (bypassing requestIdleCallback) rather than only for priority === "high". This is intentional and tested ("dispatches visible App Router prefetches without waiting for idle time"), and dedup via getPrefetchedUrls/hasPrefetchCacheEntryForNavigation guards against flooding — but it's a behavioral change worth calling out in the PR body since it affects every visible App Router <Link>, not just mismatched-prefetch cases.

  2. Multiple page entries share one probed fallback. replaceAppPageElementWithFallback applies the single probed fallback to every id from getAppPagePayloadEntryIds (page + children slots + any page-kind key). For a route with multiple distinct page/parallel-route payload entries, they'd all receive the fallback extracted from the primary page component. This is unlikely to matter for the loading-shell path (the shell is coarse-grained), and the marker-removal fallback path handles the no-replacement case correctly, but it's a mild imprecision if a route ever has genuinely divergent parallel page fallbacks.

  3. Logic reads implicitly for the inverted matcher case. A has: [{type: header, key: next-router-prefetch}] matcher (middleware runs only on prefetch) correctly returns false from middlewareMatcherConditionsMatchNavigation because clientMatcherConditionMatches returns false for that header in the has loop. Correct outcome, but it happens as a side effect of the header special-case rather than an explicit branch — a short comment there would help future readers.

None of these require changes to merge. LGTM.

@ask-bonk

ask-bonk Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2453.

Summary

I reviewed all 18 changed files in PR #2453, ran vp check on the touched source files (clean — no format/lint/type errors), and ran the five relevant unit suites (210 tests passing).

Verdict: LGTM, no blocking issues.

The change is well-implemented and stays close to Next.js behavior:

  • Matcher parity fix is correctmiddleware-runtime.ts now evaluates has/missing conditions against the unmodified request (matcherRequest), matching Next.js which evaluates matcher conditions before stripping Flight headers. The ?? options.request fallback preserves Pages Router behavior. Backed by a ported Next.js test.
  • Conservative demotion — cache reuse is only demoted for auto-mode query prefetches, and the source matcher fails closed on regex sources (correct direction).
  • Probe safety — the loading-shell fallback probe is wrapped in connection-probe + hook-warning suppression + try/catch, with recursion depth guards.

I flagged three non-blocking observations:

  1. The prefetch scheduling change (App Router prefetches now dispatch immediately instead of at idle time) is broader than the PR title suggests and worth calling out in the PR body.
  2. replaceAppPageElementWithFallback applies a single probed fallback to all page/slot entries — mild imprecision for routes with divergent parallel-route fallbacks.
  3. The inverted matcher case (has: next-router-prefetch) is handled correctly but implicitly; a comment would aid future readers.

None require changes to merge.

github run

@james-elicx
james-elicx force-pushed the codex/fix-mismatching-prefetch-28478866791 branch from fded658 to 0f4d508 Compare July 1, 2026 03:36
@james-elicx

Copy link
Copy Markdown
Member Author

/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.

Review: fix(app-router): recover mismatched prefetch rewrites

Independent review of all 18 changed files. Ran vp check on the six touched source files (clean — no format/lint/type errors) and the six relevant unit suites (1385 tests passing, incl. link-navigation, app-page-probe, app-page-dispatch, app-optimistic-routing, app-page-route-wiring, shims).

Verdict: LGTM, no blocking issues. The change is well-implemented, thoroughly tested, and stays close to Next.js behavior.

Verified correctness

  • Middleware matcher parity fix is correct and properly scoped. middleware-runtime.ts now evaluates has/missing conditions against an unmodified matcherRequest (plumbed from app-middleware.ts:249), while user middleware still receives the Flight-stripped middlewareRequest. Before this, missing: Next-Router-Prefetch matchers always "matched" on prefetch requests because the header had already been stripped, matching Next.js which evaluates matcher conditions before the strip. The options.matcherRequest ?? options.request fallback preserves behavior for every other caller. Confirmed executeMiddleware/applyAppMiddleware are App-Router-only (Pages Router has its own middleware path), so dev/prod parity holds — no equivalent change is needed elsewhere.

  • Always-setting Next-Router-Prefetch: 1 is more correct, not less. Previously the header was set only for optimistic shell prefetches; full prefetches omitted it, which would incorrectly satisfy a missing: Next-Router-Prefetch matcher on full-prefetch requests. Setting it unconditionally (link.tsx:694) matches Next.js, where all router prefetch requests carry the header. Covered by new link-navigation tests.

  • Conservative demotion is correctly failing closed. automaticSearchPrefetchMayMismatchNavigation only demotes cache reuse for mode === "auto" query-string prefetches, and clientMiddlewareSourceMatches returns false for any source containing regex/group syntax ((, \). clientMatcherConditionMatches returns "unknown" for headers it can't inspect client-side, which middlewareMatcherConditionsMatchNavigation treats as non-matching — so demotion only fires when the client is confident the matcher requires skipping prefetch. Right direction.

  • Loading-shell fallback probe follows the established pattern. probePrefetchLoadingShellFallback wraps the probe in runWithConnectionProbe + runWithSuppressedHookWarning + try/catch, the same guard stack used by app-page-execution.ts. The app-page-probe.test.ts case verifies extractFirstSuspenseFallback stops at the first Suspense fallback and does not invoke connection()-suspending children. Recursion depth guards (> 100) are present on both the server extract and the client createSuspendingElementInsideFirstSuspense.

Non-blocking observations

  1. extractFirstSuspenseFallback manually renders nested function components (app-page-dispatch.ts:147-151) by invoking value.type(props). This is more aggressive than the existing probe (which invokes only the top-level page component and lets React abort). typeof value.type === "function" also matches React class components, which would misbehave if called without new — but the whole probe is wrapped in try/catch and fails safe to null, so worst case is a missed loading shell, never a crash. Consider a short comment noting the class-component caveat.

  2. replaceAppPageElementWithFallback applies one probed fallback to every page/slot entry from getAppPagePayloadEntryIds. For a route with genuinely divergent parallel-route page fallbacks, they'd all receive the fallback extracted from the primary page. Low-impact for the coarse loading-shell path, and the marker-removal branch handles the no-replacement case, but it's a mild imprecision.

  3. Scheduling change is broader than the PR title. prefetchUrl now dispatches App Router prefetches immediately (bypassing requestIdleCallback) for all App Router links, not just priority === "high" (link.tsx:840-847). Intentional and tested ("dispatches visible App Router prefetches without waiting for idle time"), with dedup via getPrefetchedUrls/hasPrefetchCacheEntryForNavigation guarding against flooding — but worth a line in the PR body since it affects every visible App Router <Link>.

  4. Inverted matcher case is handled implicitly. A has: [{type: header, key: next-router-prefetch}] matcher (runs only on prefetch) correctly yields false from middlewareMatcherConditionsMatchNavigation because clientMatcherConditionMatches special-cases that header to false in the has loop. Correct outcome, but a one-line comment on the header branch would aid future readers.

None require changes to merge.

@ask-bonk

ask-bonk Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Reviewed all 18 changed files in PR #2453, ran vp check (clean) and the six relevant unit suites (1385 tests passing), and posted an independent review to PR #2453.

Verdict: LGTM, no blocking issues.

Key things I verified beyond the prior bot review:

  • Middleware matcher fix is real and correctly scoped. The bug was that has/missing conditions were evaluated against the Flight-stripped request, so missing: Next-Router-Prefetch always matched even on prefetch requests. The fix uses an unmodified matcherRequest. I confirmed executeMiddleware/applyAppMiddleware are App-Router-only, so dev/prod parity holds and no equivalent Pages Router change is needed.
  • Always-setting Next-Router-Prefetch: 1 is more correct than before (previously full prefetches omitted it, which would falsely satisfy missing matchers). Matches Next.js.
  • Demotion fails closed — only demotes auto-mode query prefetches when the client is confident the matcher requires skipping prefetch; regex sources and un-inspectable headers return non-matching.
  • Loading-shell probe follows the established runWithConnectionProbe + runWithSuppressedHookWarning guard pattern, and the new test proves it stops at the first Suspense fallback without invoking connection() children.

I flagged four non-blocking observations: extractFirstSuspenseFallback manually renders nested function components (class-component caveat, but fails safe via try/catch), the single probed fallback shared across page/slot entries, the broader-than-title prefetch scheduling change, and the implicitly-handled inverted matcher case. None require changes to merge.

github run

@james-elicx
james-elicx marked this pull request as ready for review July 1, 2026 03:41

@NathanDrake2406 NathanDrake2406 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.

Requesting changes on the client-side middleware matcher duplication.

The new App Router prefetch path adds another partial middleware matcher implementation in packages/vinext/src/shims/link.tsx: ClientMiddlewareMatcherObject, isClientMiddlewareMatcherObject, stripLocaleForMiddlewareMatcher, clientMiddlewareSourceMatches, and a separate has/missing condition walker. We already have the canonical middleware matcher path in packages/vinext/src/server/middleware-matcher.ts backed by packages/vinext/src/config/config-matchers.ts, and there is also a client-side Pages copy in packages/vinext/src/shims/internal/pages-data-target.ts.

I do not think link.tsx should import the server module directly, but this PR should extract the client-safe matcher subset into a shared helper and reuse it from both the App Router prefetch demotion and the Pages data target. The helper can keep the two callers' different policies explicit, e.g. fail closed for App prefetch demotion and fail open for Pages middleware data probing, while sharing matcher-object validation, locale stripping, and source matching. That preserves this PR's behavior without leaving three drift-prone matcher implementations.

@james-elicx

Copy link
Copy Markdown
Member Author

I think this PR covers the functional failure from GH Actions run 28478866791 / job 84413308650 for test/e2e/app-dir/concurrent-navigations/mismatching-prefetch.test.ts, especially mismatching prefetch recovers when a navigation rewrites to a different route than the one that was prefetched.

Why: the PR specifically handles App Router navigations where a prefetched URL rewrites to a different route, and its validation lists the focused mismatching-prefetch E2E plus the related client navigation/prefetch tests. I did not see this as a cacheComponents/use-cache-only failure.

@james-elicx

Copy link
Copy Markdown
Member Author

Backlog mapping for GitHub Actions run 28478866791 / job 84413308650: this PR appears to own the functional App Router failure in test/e2e/app-dir/concurrent-navigations/mismatching-prefetch.test.ts around mismatched prefetch rewrite recovery.

Per instruction I am skipping a duplicate implementation because this PR is by james-elicx. Remaining work looks like addressing review feedback and rerunning the exact targeted Next.js E2E proof for that path.

…-prefetch-28478866791

# Conflicts:
#	packages/vinext/src/plugins/rsc-reference-validation-normalizer.ts
#	packages/vinext/src/shims/link.tsx
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 18b4855 against base 239595e using alternating same-runner rounds. Next.js was unchanged and skipped.

1 improved · 0 regressed · 5 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 125.6 KB 126.5 KB ⚫ +0.8%
Client entry size (gzip) vinext 119.8 KB 120.0 KB ⚫ +0.1%
Dev server cold start vinext 2.63 s 2.62 s ⚫ -0.3%
Production build time vinext 3.23 s 3.17 s 🟢 -2.0%
RSC entry closure size (gzip) vinext 96.8 KB 97.4 KB ⚫ +0.6%
Server bundle size (gzip) vinext 163.1 KB 163.7 KB ⚫ +0.4%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@james-elicx

Copy link
Copy Markdown
Member Author

Run 29871622126 / job 88775791401 still fails test/e2e/app-dir/concurrent-navigations/mismatching-prefetch.test.ts at recovers when a navigation rewrites to a different route than the one that was prefetched. I think this PR owns that functional loading-shell/rewrite recovery failure.

I reverified the exact targeted Next.js v16.2.6 suite on the current-main campaign baseline (3d30a3ca3): it still fails because the click response does not contain Loading a.... I am adding this existing PR to the revival backlog rather than opening a duplicate; the refreshed branch will need the same exact targeted proof.

@james-elicx

Copy link
Copy Markdown
Member Author

Correction to my prior run mapping: apps/web/app/compatibility/suite-support.ts explicitly classifies test/e2e/app-dir/concurrent-navigations/mismatching-prefetch.test.ts as deferred Partial Prerendering. The current-main failure is genuine, but it is intentionally outside this campaign’s fix-now backlog. I am removing #2453 from the active run-29871622126 work queue; no revival action should be inferred from my earlier comment.

@james-elicx

james-elicx commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

Correction after live freshness review: this PR must not be treated as the current fix for Actions run 31290819291.

Its head 18b4855 is from 2026-07-01, is currently DIRTY/conflicting with main, and has not passed the targeted Next.js E2E against the current repository state. The current-main failure remains reproducible. The older implementation also uses structural first-Suspense fallback extraction and broad payload replacement, which do not preserve the actual React partial shell in multi-boundary/static-sibling cases.

I am therefore withdrawing the prior "skip duplicate" mapping. The run-312908 backlog item remains active until a current-main implementation has exact targeted E2E proof, clean independent review, Bonk, and CI.

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