Skip to content

fix: handle cancellation in injectRSCPayload to avoid post-abort server crash#15286

Merged
jacob-ebey merged 2 commits into
remix-run:mainfrom
MahinAnowar:fix/rsc-html-stream-cancel
Jul 9, 2026
Merged

fix: handle cancellation in injectRSCPayload to avoid post-abort server crash#15286
jacob-ebey merged 2 commits into
remix-run:mainfrom
MahinAnowar:fix/rsc-html-stream-cancel

Conversation

@MahinAnowar

Copy link
Copy Markdown
Contributor

Fixes #15275

Problem

In RSC Framework Mode, an aborted/cancelled document request can crash the production server:

TypeError: Invalid state: Unable to enqueue
    at flushBufferedChunks (...)
    at Timeout._onTimeout (...)

injectRSCPayload buffers HTML chunks and flushes them in a setTimeout(..., 0) callback. When the client aborts, the readable side of the TransformStream is cancelled while that flush is still pending — the timer then calls controller.enqueue() on a cancelled stream. Because the timer callback is async, the throw becomes an unhandled rejection, which exits the Node process. The transformer had no cancel() hook, so the pending timer and the RSC payload stream were never cleaned up.

Fix

  • Add a cancel(reason) handler to the transformer: it flags cancellation, clears the pending flush timer, drops the buffered chunks, cancels the underlying RSC payload stream (via its reader when streaming has started), and resolves the flight-data promise.
  • Guard the flush timer callback and the writeRSCStream write loop so late timers / in-flight reads become no-ops after cancellation instead of enqueueing into a cancelled stream.

The transformer is typed with a small extension (Transformer & { cancel? }) since the repo's current TS lib doesn't yet include the standard Transformer#cancel member.

Tests

Added packages/react-router/__tests__/rsc/html-stream-test.ts with two regression tests, covering an abort while a flush is pending (the reported crash) and an abort mid-RSC-stream. Both assert no unhandled rejection escapes a timer and that the RSC payload stream gets cancelled.

  • Without the fix: both fail (the flush-pending case reproduces the exact Invalid state: Unable to enqueue rejection).
  • With the fix: both pass. Typecheck, ESLint and Prettier are clean. (Note: the sibling rsc/server-test.ts suite currently fails to load on a clean main checkout in the same way, unrelated to this change.)

A change file is included (patch.rsc-html-stream-cancel.md), and I've signed the CLA in contributors.yml as part of this PR.

When a client aborts a document request, the readable side of the
injectRSCPayload TransformStream is cancelled while a buffered flush
(setTimeout(..., 0)) may still be pending. The timer then enqueues into
a cancelled stream, throwing "TypeError: Invalid state: Unable to
enqueue" from a timer callback — an unhandled rejection that kills the
server process.

Add a cancel() handler that clears the pending flush, drops the buffer,
and cancels the underlying RSC payload stream, plus cancellation guards
on the flush timer and the RSC writer loop.

Fixes remix-run#15275
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

✅ CLA Signed

Thanks for signing the Contributor License Agreement.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

✅ Change File Found

One or more change files found.

Type Change
patch Fix server crash (TypeError: Invalid state: Unable to enqueue) when a request is aborted while the RSC HTML stream has a pending flush — injectRSCPayload now handles cancellation of its readable side, clears the pending flush, and cancels the underlying RSC payload stream

@jacob-ebey
jacob-ebey merged commit e493cdc into remix-run:main Jul 9, 2026
12 checks passed
@ryanflorence ryanflorence mentioned this pull request Jul 9, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Hello there,

We recently published version 8.3.0 which includes this pull request. If you'd like to take it for a test run please try it out and let us know what you think!

Thanks!

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.

RSC SSR stream can crash with TransformStreamDefaultController.enqueue after aborted request

3 participants