Segment prefetch of an ISR notFound() page never completes in Chromium when a service worker mediates fetches (wedges connections, networkidle never reached)
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/moclippa/next-isr-notfound-prefetch-hang
To Reproduce
npm install && npx playwright install chromiumnpm run build(fresh production build) andnpm startnode probe.mjs— exits 1 when reproduced (it is, deterministically, on a fresh build)
Or by hand: open Chrome DevTools → Network on http://localhost:3000/, reload once (so the service worker controls the page), and watch the segment prefetches of the dead link stay pending forever.
The setup is nothing exotic:
- an on-demand ISR route (
app/[locale]/(app)/card/[id]/page.tsx):generateStaticParams()returns[],revalidate = 3600, unknown ids callnotFound(); - a home page with a
<Link>to an id that doesn't exist; - a bog-standard PWA offline-shell service worker (
public/sw.js, stale-while-revalidate viaevent.respondWith(fetch(request))).
Current vs. Expected behavior
Current: with the service worker controlling the page, the router's segment prefetches of the dead link (RSC: 1, Next-Router-Prefetch: 1, Next-Router-Segment-Prefetch: /_tree …) receive response headers (chunked, x-nextjs-cache: HIT, x-nextjs-postponed: 2) and then never complete. Each wedged request permanently occupies a connection — with several such prefetches the whole 6-connection pool for the origin is exhausted — and networkidle is never reached, which breaks Playwright/Lighthouse and keeps loading indicators alive. The identical request from curl/Node fetch() completes in single-digit milliseconds while the browser's copies are still pending.
Expected: a prefetch to a page that 404s completes (or is cancelled) like any other request and never wedges a connection.
What we verified while isolating it (details and scripts in the repro repo):
- A TCP tap between Chromium and
next startshows the server completes the response, terminating chunk included. The server is not the problem. - CDP shows Chromium receives every body byte (
Network.dataReceivedaccounts for the full payload) butNetwork.loadingFinishednever fires — even after forcing GC viaHeapProfiler.collectGarbage. - The page-side consumer is the segment cache (
next/dist/client/components/segment-cache/cache.js). On a prefetch response it decides not to use — e.g. infetchRouteOnCacheMiss:if (!response || !response.ok || …) { rejectRouteCacheEntry(entry, …); return null; }— it abandonsresponse.bodywithout reading or cancelling it. The same pattern exists on the other reject paths (fetchSegmentEntryOnCacheMiss,fetchSegmentPrefetchesUsingDynamicRequest). - Without a service worker the leak is invisible in practice: Chromium buffers the small completed response and finalizes the request even though nobody read it. With a service worker mediating the fetch, an unconsumed
respondWithbody legitimately keeps both the outer and the SW-side request in-flight forever — so every abandoned prefetch response wedges a connection permanently.
The fix would be to drain or cancel (response.body.cancel()) on every segment-cache path that rejects a prefetch response.
Notes:
- Found in a production PWA where the home screen links cards that only a live upstream can resolve; 15 Playwright tests went red on
networkidlebefore this was isolated. In that app the wedged response is the ISR-cached 404 itself (404+x-nextjs-cache: HIT+x-nextjs-postponed: 2); in the trimmed repro the wedged responses are segment answers served from the prefetch-seeded 404 entry — the common thread is the abandoned body. export const dynamic = 'force-dynamic'on the route does not help (the client can't know the rendering mode before it asks).prefetch={false}on the offending links is the workaround we shipped.
Provide environment information
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin 25.5.0
Available memory (MB): 36864
Available CPU cores: 14
Binaries:
Node: 26.6.0
npm: 11.18.0
Relevant Packages:
next: 16.2.12 (also reproduced on 16.3.1-canary.4)
react: 19.2.4
react-dom: 19.2.4
typescript: 5.9.3
Browser: Playwright Chromium 151 (headless; also observed headed)
Which area(s) are affected? (Select all that apply)
Navigation, Partial Prerendering (PPR), Service Workers / PWA
Which stage(s) are affected? (Select all that apply)
next start (local), Other (platform)
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 at moclippa/next-isr-notfound-prefetch-hang, run its npm install, build, start, and node probe.mjs steps, then inspect next/dist/client/components/segment-cache/cache.js. Trace fetchRouteOnCacheMiss, fetchSegmentEntryOnCacheMiss, and fetchSegmentPrefetchesUsingDynamicRequest; done means rejected prefetch responses no longer leave Chromium requests pending with the service worker enabled and the probe reaches completion.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, nextjs, playwright
- Domain
- frontend, testing-qa, web-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100