vercel / vercel/next.js

Segment prefetch of an ISR notFound() page never completes in Chromium when a service worker mediates fetches (wedges connections, networkidle never reached)

Open
#96,801 1 comment 0 reactions 0 assignees View on GitHub

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
  1. npm install && npx playwright install chromium
  2. npm run build (fresh production build) and npm start
  3. node 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 call notFound();
  • 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 via event.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 start shows the server completes the response, terminating chunk included. The server is not the problem.
  • CDP shows Chromium receives every body byte (Network.dataReceived accounts for the full payload) but Network.loadingFinished never fires — even after forcing GC via HeapProfiler.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. in fetchRouteOnCacheMiss: if (!response || !response.ok || …) { rejectRouteCacheEntry(entry, …); return null; } — it abandons response.body without 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 respondWith body 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 networkidle before 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.