Problem
A same-origin RSC response-URL redirect (response.url differs from the requested URL) leaves the original response's ReadableStream un-cancelled and unread when the loop re-fetches the redirected href. On Cloudflare Workers / fetch implementations that hold the connection open until the body is consumed or cancelled, this leaks a stream/connection per such redirect hop. The streamed-header redirect path already cancels, indicating the response-url path likely should too.
Evidence
packages/vinext/src/server/app-browser-entry.ts:1973 — On liveFetchDecision.kind === 'followRedirect', the body is cancelled only when discardBody is true; the loop then continues and reassigns navResponse, abandoning the prior response stream.
packages/vinext/src/server/navigation-planner.ts:464 — createRscFetchResultFollowRedirectDecision is invoked for response-url redirects with discardBody:false (lines 464-476), while the streamed-header redirect path passes discardBody:true (lines 499-510).
Next.js behavior (Next.js has the SAME defect (acknowledged TODO in their source))
I traced the vinext code path (app-browser-entry.ts:1973 follow-redirect with discardBody:false from navigation-planner.ts:464-476 for the response-url signal) to its Next.js counterpart in fetch-server-response.ts createFetch. Next.js detects the same response-URL redirect via browserResponse.redirected (:575,:580) and, in the redirect-replay loop, re-fetches the redirected URL and reassigns browserResponse at :612 — abandoning the prior browserResponse.body ReadableStream without ever calling .cancel(). The source even carries an explicit // TODO: We should abort the previous request. at :604, confirming Next.js knowingly leaves the prior request/body un-cancelled. The only .cancel() calls in the file apply to the segment-cache teed clone, and the optional signal is a navigation-wide abort, neither of which cleans up the abandoned redirect response. This is the identical defect: a same-origin redirect hop leaks an un-cancelled response stream, which matters on fetch implementations (e.g. Cloudflare Workers) that hold the connection until the body is consumed or cancelled.
Citations: packages/next/src/client/components/router-reducer/fetch-server-response.ts:575-615 (manual redirect-replay loop); :604 (// TODO: We should abort the previous request.); :612 (browserResponse = await fetchPromise reassigns/abandons the prior response without .cancel()); :492-649 (createFetch, the counterpart to vinext app-browser-entry's RSC fetch loop). grep for cancel/abort/signal shows the only .cancel() calls (:443,:461) are for the segment-cache teed clone, not the abandoned redirect response body; the optional signal (:497,:530) is a whole-navigation abort, not a per-ho
Suggested fix
Cancel navResponse.body before continuing on a response-url followRedirect (mirror the discardBody cancellation), or set discardBody:true for response-url redirects in createRscFetchResultFollowRedirectDecision since the body is always discarded by re-fetching the redirect target.
Test plan
Unit: response-url redirect path cancels the prior body (body.cancel spy).
Related / notes
UPSTREAM SHARES THE DEFECT: Next.js fetch-server-response.ts:604 has a literal // TODO: We should abort the previous request. and reassigns at :612 without cancel. Fixing in vinext is still right (Workers holds connections until body consumed/cancelled) — and this is a candidate upstream Next.js PR. BLOCKED on PR #1962.
Found via a deep source audit of main @ fd10233 (2026-06-12). Behavior parity-checked against Next.js v16.3.0-canary.7 source; citations above reference packages/next/src/... in the Next.js repo. Screened against all open issues/PRs as of 2026-06-12 to avoid duplicating tracked work.
Problem
A same-origin RSC response-URL redirect (response.url differs from the requested URL) leaves the original response's ReadableStream un-cancelled and unread when the loop re-fetches the redirected href. On Cloudflare Workers / fetch implementations that hold the connection open until the body is consumed or cancelled, this leaks a stream/connection per such redirect hop. The streamed-header redirect path already cancels, indicating the response-url path likely should too.
Evidence
packages/vinext/src/server/app-browser-entry.ts:1973— On liveFetchDecision.kind === 'followRedirect', the body is cancelled only when discardBody is true; the loop then continues and reassigns navResponse, abandoning the prior response stream.packages/vinext/src/server/navigation-planner.ts:464— createRscFetchResultFollowRedirectDecision is invoked for response-url redirects with discardBody:false (lines 464-476), while the streamed-header redirect path passes discardBody:true (lines 499-510).Next.js behavior (Next.js has the SAME defect (acknowledged TODO in their source))
I traced the vinext code path (app-browser-entry.ts:1973 follow-redirect with discardBody:false from navigation-planner.ts:464-476 for the response-url signal) to its Next.js counterpart in fetch-server-response.ts
createFetch. Next.js detects the same response-URL redirect viabrowserResponse.redirected(:575,:580) and, in the redirect-replay loop, re-fetches the redirected URL and reassignsbrowserResponseat :612 — abandoning the priorbrowserResponse.bodyReadableStream without ever calling.cancel(). The source even carries an explicit// TODO: We should abort the previous request.at :604, confirming Next.js knowingly leaves the prior request/body un-cancelled. The only.cancel()calls in the file apply to the segment-cache teed clone, and the optionalsignalis a navigation-wide abort, neither of which cleans up the abandoned redirect response. This is the identical defect: a same-origin redirect hop leaks an un-cancelled response stream, which matters on fetch implementations (e.g. Cloudflare Workers) that hold the connection until the body is consumed or cancelled.Citations: packages/next/src/client/components/router-reducer/fetch-server-response.ts:575-615 (manual redirect-replay loop); :604 (
// TODO: We should abort the previous request.); :612 (browserResponse = await fetchPromisereassigns/abandons the prior response without.cancel()); :492-649 (createFetch, the counterpart to vinext app-browser-entry's RSC fetch loop). grep forcancel/abort/signalshows the only.cancel()calls (:443,:461) are for the segment-cache teed clone, not the abandoned redirect response body; the optionalsignal(:497,:530) is a whole-navigation abort, not a per-hoSuggested fix
Cancel navResponse.body before continuing on a response-url followRedirect (mirror the discardBody cancellation), or set discardBody:true for response-url redirects in createRscFetchResultFollowRedirectDecision since the body is always discarded by re-fetching the redirect target.
Test plan
Unit: response-url redirect path cancels the prior body (body.cancel spy).
Related / notes
UPSTREAM SHARES THE DEFECT: Next.js fetch-server-response.ts:604 has a literal
// TODO: We should abort the previous request.and reassigns at :612 without cancel. Fixing in vinext is still right (Workers holds connections until body consumed/cancelled) — and this is a candidate upstream Next.js PR. BLOCKED on PR #1962.Found via a deep source audit of
main@ fd10233 (2026-06-12). Behavior parity-checked against Next.js v16.3.0-canary.7 source; citations above referencepackages/next/src/...in the Next.js repo. Screened against all open issues/PRs as of 2026-06-12 to avoid duplicating tracked work.