router.prefetch redirect loop
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 142k
- Forks
- 32.4k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 351
Description
Link to the code that reproduces this issue
https://github.com/skurekjakub/next-rsc-prefetch-redirect-loop
To Reproduce
npm install && npx playwright install chromium- Run
npm run repro.
The script builds the project, launchesnext start, loads/docs/alpha, clicks the button triggeringrouter.prefetch("/docs/changelog", { kind: "full" })once, and counts incoming_rscrequests. It exits with code 1 because requests keep firing continuously (hundreds per second) 20 seconds later. - Manual reproduction:
- Run
npm run build && npm start. - Open
http://localhost:3000/docs/alphain Chrome. - Open DevTools Network tab and filter by
_rsc. - Click the button triggering
router.prefetch("/docs/changelog", { kind: "full" }). - Watch the counter and network tab flood with requests (~700 req/sec).
- Run
- Control check:
- Run
npm run repro:control. This invokesrouter.prefetch("/docs/changelog")without{ kind: "full" }. No loop happens.
- Run
Current vs. Expected behavior
Current behavior
Triggering router.prefetch(url, { kind: "full" }) once causes the browser tab to fire _rsc requests endlessly until closed (~700 requests/second on localhost).
Each round consists of:
308redirect for/docs/changelog200response for/changelog/_treerequests for every visible<Link>on the page
router.prefetch("/docs/changelog", { kind: "full" })
_rsc requests: 4 at load, 6,722 after 10 s, 13,534 after 20 s
308 responses for /docs/changelog: 1,696
In-page counter: 11,837
Requests by path & status:
/ 200 1,698
/changelog 200 3,393
/docs/alpha 200 1,696
/docs/beta 200 1,696
/docs/changelog 308 1,696
/docs/delta 200 1,662
/docs/gamma 200 1,696
LOOP RATE: ~681 requests/second between 10s and 20s after a single prefetch call.
In production on our documentation site (Next 16.3.3, self-hosted on Azure App Service), a single user hovering a sidebar link produced ~190,000 requests (5.8 GB transferred) in 100 minutes. Another session recorded 22,000 requests in 20 minutes. The loop only stopped when the user closed the tab.
Expected behavior
The client router should follow the redirect once, record that the destination has a different structure / is a rewrite, and stop. Prefetch retries must have an upper bound. A normal <Link> prefetch for the same destination finishes after one attempt; imperative router.prefetch should do the same.
Provide environment information
Operating System:
Platform: linux
Arch: x64
Node: 24.18.0
npm: 12.0.1
Relevant Packages:
next: 16.3.3 (also reproduced on 16.3.4 and 16.4.0-canary.20)
react: 19.2.8
react-dom: 19.2.8
typescript: 5.9.3
Next.js Config:
cacheComponents: true
experimental.partialPrefetching: true
Which area(s) are affected? (Select all that apply)
Redirects
Which stage(s) are affected? (Select all that apply)
Other (Deployed), next start (local)
Additional context
- Also reproduces on
next@16.3.4(711 req/s) andnext@16.4.0-canary.20(714 req/s). - Requires both
cacheComponentsandpartialPrefetching. - Only occurs on production builds (
next start);next devdoes not prefetch like this. - Workaround: We added a build gate forbidding sidebar links to redirect sources, and avoided passing
{ kind: 'full' }torouter.prefetch.
Root Cause Analysis
Tracing through packages/next/src/client/components/segment-cache/ in next@16.3.3:
-
No component lifecycle cancellation:
router.prefetchtasks are not associated with any<Link>element, so standard cancellation via unmount (links.ts) never fires. -
Optimistic route matching guesses the layout:
When prefetching/docs/changelog,matchKnownRouteinoptimistic-routes.tschecks existing patterns, sees it matches[collection]/[...slug], and synthesizes an optimistic route entry based on that pattern instead of requesting a/_treefirst. -
Tree divergence on redirect:
WithpartialPrefetchingenabled,fetchSegmentPrefetchesUsingDynamicRequestincache.tssends an RSC request with the synthetic tree. The server returns a 308 redirect to/changelog. The browser follows it and receives the/changelogresponse. The client router detects that the/changelogtree diverged from the guessed[collection]/[...slug]tree. -
Self-invalidating cache bump:
Incache.ts(L3313-L3324):- The router calls
markRouteEntryAsDynamicRewrite(route). - It calls
invalidateRouteCacheEntries(), which incrementscurrentRouteCacheVersion. (A comment here says"It can't loop", followed by aTODOnoting unbounded retries). - It rejects pending segment entries.
- The router calls
-
The loop closes:
- In
cache-map.ts(isValueExpired) andoptimistic-routes.ts(readPattern), anything older thancurrentRouteCacheVersionis expired. - Bumping the version discards the dynamic-rewrite note that was just recorded, along with cached trees for visible links.
- Visible links immediately re-request their
/_tree, which re-learns the base[collection]/[...slug]pattern. - The active imperative prefetch task retries, makes the exact same optimistic guess, hits the redirect again, and loops infinitely.
- In
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the reproduction repository and run npm run repro, then inspect packages/next/src/client/components/segment-cache/cache.ts, cache-map.ts, optimistic-routes.ts, and links.ts. Trace the full prefetch path for router.prefetch("/docs/changelog", { kind: "full" }) and compare it with the control case. Done means the redirect is handled without an unbounded _rsc request loop while normal prefetch behavior remains intact.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, nextjs, playwright, react
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 50/100