cloudflare / cloudflare/vinext

App Router: propagate shell errors when resuming Node FizzStream (PPR/Cache Components redirect-from-empty-shell)

Open
#1,918 0 comments 0 reactions 0 assignees View on GitHub
nextjs-tracking
Dominant language
TypeScript
Stars
8.8k
Forks
406
Avg merge
2d 6h
Merged PRs (30d)
120

Description

## Next.js Change

**Commit:** [`e053b75`](https://github.com/vercel/next.js/commit/e053b759cdada06c4c877273ab505401a2a24071)
**PR:** [#94668](https://github.com/vercel/next.js/pull/94668)

## What changed

A production bug fix for the Node resume path used by PPR / Cache Components. When an empty prerendered shell (one that postponed everything) resumes into a redirect, React reports the failure via `onShellError`. Previously the Node resume adapter (`resumeToFizzStream`) returned its `PassThrough` stream **as soon as the pipe was wired up**, without observing whether React actually produced a resumable HTML shell. The downstream app-render code then proceeded into Flight injection and waited forever for an HTML chunk that would never arrive, hanging the response.

The fix mirrors what the Web stream resume path already does:

1. Add a `DetachedPromise` for the shell (`shellReady`).
2. In the `onShellReady` callback, resolve `shellReady`; in `onShellError`, reject it (after passing through to any caller-provided handlers).
3. After `pipeable.pipe(pt)`, `await shellReady.promise` before returning the stream.

A rejected `shellReady` now propagates through the existing app-render recovery path, producing a complete error document that carries the redirect signal, instead of an indefinitely-pending response.

### Files changed (non-test)

- `packages/next/src/server/app-render/stream-ops.node.ts` (+11/-0) — wires `onShellReady` / `onShellError` into a promise and awaits it before returning the stream from `resumeToFizzStream`.

### Repro shape (from upstream test fixture)

An app with PPR enabled where the prerendered shell postpones the entire body (e.g. a layout that wraps a single dynamic page in ``), plus a route that performs a server-side `redirect()` at the top of its render — when resumed at request time, the empty shell shell-errors with the redirect, and the response hung prior to this fix.

## Impact on vinext

vinext's App Router production server delegates the actual render to the built RSC entry, which calls into React's resume APIs via the Vite RSC plugin. The bug only surfaces if vinext implements the **Node resume** (`resumeToPipeableStream` / Fizz Node) path for PPR / Cache Components — the Web Streams resume path was already correct.

Things to check in vinext:

1. **App Router prod path (`server/prod-server.ts`, `entries/app-rsc-entry.ts`, `server/app-*.ts`):** if any resume helper builds a `PassThrough` and pipes `pipeable` into it, the same `shellReady` `DetachedPromise` + `await` pattern must be applied. Returning the stream before the shell has either resolved or errored will hang on any shell-time error (notably redirects out of an empty shell).
2. **Cloudflare Workers (`cloudflare/worker-entry.ts`):** Workers uses Web Streams, so the equivalent Web-side codepath should already handle `onShellError`. Worth double-checking that vinext's worker entry doesn't accidentally swallow shell errors when resuming.
3. **Dev parity:** the same pattern lives in vinext's dev server (`server/dev-server.ts` for Pages Router; the App Router dev path via `entries/app-rsc-entry.ts`). `AGENTS.md` calls out dev/prod parity explicitly — if a Node resume helper exists in dev too, it needs the same fix.

The diff itself is small (~11 lines) and the pattern is straightforward; the value of this issue is making sure both runtimes (Node and Workers) and both modes (dev and prod) are audited together.

## Related

- #1747 — App Shell extraction via byte offsets (PPR plumbing)
- #1696 — Cache Components error recovery RSC payload

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.