Skip to content

fix(app-router): cancel live RSC redirect bodies before refetch#2559

Open
NathanDrake2406 wants to merge 1 commit into
cloudflare:mainfrom
NathanDrake2406:nathan/rsc-redirect-body-cancel
Open

fix(app-router): cancel live RSC redirect bodies before refetch#2559
NathanDrake2406 wants to merge 1 commit into
cloudflare:mainfrom
NathanDrake2406:nathan/rsc-redirect-body-cancel

Conversation

@NathanDrake2406

Copy link
Copy Markdown
Contributor

Overview

Field Details
Goal Close #1987 by releasing abandoned live RSC response bodies before response-URL redirect replay.
Core change Response-URL followRedirect decisions now set discardBody: true for live fetch results. Cached-source redirect decisions stay non-discarding because they have no live stream to release.
Boundary navigation-planner owns the discard decision. app-browser-entry already owns the effect by cancelling bodies for discarded fetch results before continuing.
Primary files navigation-planner.ts updates the planner contract. navigation-planner-rsc-fetch-result.test.ts covers live and cached redirect decisions.
Expected impact Same navigation target, history, and trace behavior. Better stream and connection cleanup on fetch implementations that keep unread bodies open until consumed or cancelled.

Why

RSC redirect replay is a resource-ownership boundary: once the client decides to ignore a live response and fetch the redirected target, it no longer has a consumer for the original body. The planner already tells the browser executor when streamed-header redirect bodies are discarded; live response-URL follow hops need the same signal so the executor can release the unread body before the next fetch.

Area Principle / invariant What this PR changes
Live response-URL redirect replay A live body abandoned by redirect replay must be released before the next request starts. Marks live response-URL followRedirect decisions as discardBody: true.
Planner and executor split The planner returns a pure decision; the browser executor performs effects. Keeps cancellation in the existing app-browser-entry discard path instead of adding a second effect path.
Cached response reuse Cached-source decisions do not represent an owned live response stream. Preserves discardBody: false for cached response-URL redirect decisions.

What changed

Scenario Before After
Live response-URL follow redirect Planner returned discardBody: false; executor continued the redirect loop without cancelling the old body. Planner returns discardBody: true; existing executor code cancels the old body before refetching the target.
Cached response-URL follow redirect Planner returned discardBody: false. Unchanged, because no live stream is being abandoned.
Streamed-header redirects Planner returned discardBody: true. Unchanged.

Fixes #1987.

Validation
  • vp test run tests/navigation-planner-rsc-fetch-result.test.ts -t "follows live response-URL redirects and requires body discard"
    • Red phase before source change: failed with discardBody: false.
    • Green phase after source change: passed.
  • vp test run tests/navigation-planner-rsc-fetch-result.test.ts
  • vp test run tests/app-browser-entry.test.ts -t "app browser RSC redirect lifecycle"
  • vp test run tests/navigation-planner-rsc-fetch-result.test.ts tests/app-browser-entry.test.ts
  • vp check
  • git diff --check
  • Commit hook also ran staged checks, full check, staged unit and integration tests, and Knip.
Risk / compatibility
  • Public API: none. This changes an internal planner decision consumed by the existing browser executor.
  • Runtime behavior: redirect target, redirect depth, history mode, previous-next URL, and traces are unchanged.
  • Resource behavior: live unread response bodies are now cancelled before response-URL redirect replay starts the next fetch.
  • Next.js compatibility: current Next.js canary still has a TODO to abort the previous request in its equivalent redirect replay loop. This PR intentionally hardens vinext for Workers-compatible fetch runtimes while keeping navigation semantics aligned.
Non-goals
  • No change to invalid-payload, compatibility-mismatch, or terminal response-URL hard-navigation body handling.
  • No change to redirect depth limits, external redirect handling, or streamed-header redirect semantics.
  • No attempt to change Next.js itself in this PR.

References

Reference Why it matters
#1987 Tracks the unread body leak on response-URL RSC redirect replay.
Next.js fetch-server-response redirect replay Shows the equivalent Next.js redirect replay loop and the existing abort TODO.
Next.js CDN cache-busting redirect test Confirms the intended response-URL redirect/cache-busting semantics, separate from stream cleanup.

Live response-URL RSC redirect hops fetch the redirected target in a new navigation-loop iteration. The abandoned response body remained unread because the planner marked those hops as non-discarding, so Workers-compatible fetch implementations could keep the underlying stream open.

Mark live response-URL follow decisions as body-discarding while leaving cached-source decisions unchanged because they have no live stream to release. The browser executor already cancels discarded bodies before continuing the redirect loop.

The planner regression now asserts live response-URL redirects require body discard and preserves cached redirect reuse.
@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review July 8, 2026 15:53
@pkg-pr-new

pkg-pr-new Bot commented Jul 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2559
npm i https://pkg.pr.new/create-vinext-app@2559
npm i https://pkg.pr.new/vinext@2559

commit: 4f9d167

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 4f9d167 against base aa1b666 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 126.6 KB 126.6 KB ⚫ +0.0%
Client entry size (gzip) vinext 120.6 KB 120.6 KB ⚫ +0.0%
Dev server cold start vinext 2.72 s 2.72 s ⚫ +0.2%
Production build time vinext 3.18 s 3.18 s ⚫ +0.1%
RSC entry closure size (gzip) vinext 98.5 KB 98.5 KB ⚫ -0.0%
Server bundle size (gzip) vinext 165.1 KB 165.1 KB ⚫ +0.0%

View detailed results and traces

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

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.

App Router: response-URL redirect follow abandons the prior RSC body without cancel() (Next.js shares this bug)

1 participant