Skip to content

Prevent streaming disconnects from crashing Node - #15324

Open
brophdawg11 wants to merge 1 commit into
mainfrom
brophdawg11/fix-node-writable-close
Open

Prevent streaming disconnects from crashing Node#15324
brophdawg11 wants to merge 1 commit into
mainfrom
brophdawg11/fix-node-writable-close

Conversation

@brophdawg11

Copy link
Copy Markdown
Contributor

Client disconnects can close a Node writable while a response is still streaming. The stream pump then destroys the writable with an error after its monitor has removed the error listener, allowing the asynchronous error event to terminate the process.

This keeps destroy errors handled for both readable streams and async iterables, while avoiding redundant destruction of already-destroyed writables.

Closes #15287

Assisted-By: devx/06090486-2b20-4fb8-99f0-cdf7db642619
@github-actions

Copy link
Copy Markdown
Contributor

Preview Build Available

Preview builds have been created for this PR. You can install react-router using:

pnpm install "remix-run/react-router#preview/pr-15324&path:packages/react-router"

And/or install other packages via:

pnpm install "remix-run/react-router#preview/pr-15324&path:packages/react-router-dev"
pnpm install "remix-run/react-router#preview/pr-15324&path:packages/react-router-express"
pnpm install "remix-run/react-router#preview/pr-15324&path:packages/react-router-node"
pnpm install "remix-run/react-router#preview/pr-15324&path:packages/react-router-serve"

These preview builds will be updated automatically as you push new commits.

@github-actions

Copy link
Copy Markdown
Contributor

✅ CLA Signed

Thanks for signing the Contributor License Agreement.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Change File Found

One or more change files found.

Type Change
patch fix: Prevent client disconnects during streaming from crashing the Node process

@brophdawg11
brophdawg11 marked this pull request as ready for review July 16, 2026 14:42
}
} catch (error: unknown) {
try {
reader.cancel(error).catch(() => {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This covers the res side, but reader.cancel(error) here still passes the same
synthetic close error upstream. It goes through createReadableStreamFromReadable's
StreamPump.cancel into body.destroy(error) on the source stream. If there's a
Transform between the render and body (CSP nonce injection, or
@sentry/react-router's getMetaTagTransformer). React's .pipe() never put an
'error' listener on that stream, so the destroy throws the same unhandled 'error',
just on the upstream stream instead of the response. That's the Sentry stack marilari88
posted in #15287.

I applied this change on top of 8.2.0 and ran it through a real bare-express pipeline with
a transform in entry.server and got a real client abort mid-stream:

SERVER EXIT code 1
Error: Writable closed before stream finished
    at ServerResponse.onClose (@react-router/node/dist/index.js)
Emitted 'error' event on PassThrough instance

Repro: https://github.com/guiterguy219/react-router-15287-repro

Same class of fix covers it: cancel the reader without the synthetic reason, or tag the
benign close in onClose and skip both destroy and the error-carrying cancel for that
tag, while real write/render errors still destroy + throw.

@andrezani

Copy link
Copy Markdown

This pull request is one approval away from merging a high-severity bug.

remix-run/react-router · PR #15324 — Prevent streaming disconnects from crashing Node — was open right now. We ran PR Quorum's review panel over the same diff, read-only, live. Total cost: $0.0011. Here is what it caught:


🟠 destroyWritable may silently swallow legitimate writable errors

packages/react-router-node/stream.ts:60 · Correctness reviewer · High · 95% confidence

In remix-run/react-router · Prevent streaming disconnects from crashing Node.

The destroyWritable function attaches a no-op once('error') listener before calling writable.destroy(error). This listener consumes the asynchronous 'error' event that Node emits when destroy() is called with an error on a writable that is not yet destroyed. However, if the writable is already destroyed (the early return on line 61-63), the function returns without attaching the error listener. In that case, if writable.destroy(error) was called elsewhere (or the writable was destroyed by some other mechanism) and the error event fires asynchronously, there is no listener to catch it, potentially crashing the process. The early return should also attach a no-op error listener to the already-destroyed writable to ensure any pending error events are consumed.

Suggested fix

function destroyWritable(writable: Writable, error: Error) {
  if (writable.destroyed) {
    // Ensure any pending error events from a previous destroy are consumed
    writable.once("error", () => {});
    return;
  }

  // The write promise carries this error to the caller. Also consume the
  // asynchronous error event from destroy so it cannot crash the process
  // after the writable error monitor has been cleaned up.
  writable.once("error", () => {});
  writable.destroy(error);
}

This review runs on your PRs before they merge.Install PR Quorum


The receipts

  • Read-only. These PRs were open at capture time; we reviewed the live diff without posting anything. Maintainers can still catch these the ordinary way — installed, this review would already be sitting on the PR.
  • Unedited. These are raw outputs from the default panel (Correctness, Security, Architecture) on the default model. Some findings will be wrong — that's the honest state of AI review, shown rather than hidden.

remix-run/react-router merges hundreds of pull requests a year. At about $0.0011 a review, checking every one of them costs a few dollars.

Run this panel on your own pull requests

Three reviewers + a fact-checking verifier, one combined review, posted before merge — never after.
Install PR Quorum · free tier · 2-minute setup · advisory only, your team keeps merge control

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client abort mid-stream can crash the Node process: unhandled 'error' from writable.destroy() after monitor cleanup (@react-router/node >= 8.0.0)

4 participants