Skip to content

fix(server): stop reporting client-aborted renders through onRequestError#2665

Open
jlucaso1 wants to merge 6 commits into
cloudflare:mainfrom
jlucaso1:fix/skip-reporting-client-aborts
Open

fix(server): stop reporting client-aborted renders through onRequestError#2665
jlucaso1 wants to merge 6 commits into
cloudflare:mainfrom
jlucaso1:fix/skip-reporting-client-aborts

Conversation

@jlucaso1

Copy link
Copy Markdown

Closes #2664

When the response consumer goes away mid-stream (client disconnect, aborted navigation), spec compliant runtimes cancel the response body with no reason. React turns that into Error("The render was aborted by the server without a reason.") for every pending task, and the render onError reported it through onRequestError as if it were a real failure. On a production app with Sentry wired to onRequestError this produced hundreds of noise events per hour, all with response status 200. Next.js swallows exactly this class (isAbortError / ResponseAborted in pipe-readable.ts).

Change

  • server/response-aborted.ts: a named ResponseAbortedError (mirroring Next's ResponseAborted) plus tagConsumerCancellation(stream), a passthrough that forwards an explicit cancel reason unchanged and replaces a reason-less cancel with the tagged error. Built with highWaterMark: 0 so it stays pull-on-demand with no extra buffering.
  • buildAppPageRscResponse / buildAppPageHtmlResponse wrap the render stream with tagConsumerCancellation right before it becomes the Response body, so the tag applies on every runtime (Node, Workers, any host that cancels the web stream).
  • createRscOnErrorHandler and the SSR onError classify the tagged abort as expected control flow: no reportRequestError, no error meta, digest still returned for React.

Real render errors are untouched: only cancellations initiated by the response consumer are reclassified, and the existing "reports server component render errors via instrumentation" test still passes.

Related cleanup: latent unhandled rejections around pipeThrough

The new coverage surfaced a latent class of unhandled rejections in the streaming path: pipeThrough's internal pipeTo loop can leave an in-flight writer.write (or its own pipe promise) rejecting unhandled when the stream is cancelled between chunks, and the drain-detection wrappers rejected with the raw cancel reason (reproducible in ~20 lines of plain Node).

  • deferUntilStreamConsumed and the sendAppPageResponse wrapper used pipeThrough(new TransformStream({ flush })) only to detect clean drain; both now use a manual passthrough and fire their flush callback from the done branch, removing a TransformStream plus pipe machinery per response.
  • The real transforms (normalizeReactFlightPreloadHints, the SSR tick-buffered embed) now run through a small pumpThrough helper (server/stream-pump.ts) that owns every read/write promise and propagates failures both ways, replacing pipeThrough.

With the removed layers, the net per-chunk overhead of this PR is at or below main, and the affected suites now run with zero unhandled errors (they previously reported them on cancellation paths).

Tests

  • New integration test reproduces the bug end to end: RSC request against the built app-basic entry, read one chunk, cancel the body, then assert the instrumentation recorder stays empty. Fails on main with "The render was aborted by the server without a reason." recorded; passes with this change.
  • Unit tests: a consumer-cancellation abort is not reported and still yields a digest; tagConsumerCancellation tags a reason-less cancel and forwards explicit reasons unchanged.
  • app-router-production-server (including the pre-existing instrumentation reporting test), app-flight-framing-production, app-page-dispatch, app-page-render, app-rsc-error-handler, app-rsc-errors and app-router-dev-server pass with zero unhandled errors; tsc is clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92de6f4a1e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/server/stream-pump.ts
Comment thread packages/vinext/src/server/response-aborted.ts Outdated
Comment thread packages/vinext/src/server/response-aborted.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eded7a196d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/server/app-page-response.ts
Comment thread packages/vinext/src/server/stream-pump.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4bcb9b92b8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/server/app-page-response.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: abf4007868

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

),
cleanup,
() => {
rscEmbed.abort?.();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Cancel the SSR Flight branch when abandoning HTML

When a document consumer disconnects while a Server Component is suspended, this only cancels rscEmbed's auxiliary branch. The other branch created for createFromReadableStream remains actively reading, and aborting the Fizz HTML render does not cancel that independent Flight reader, so the underlying RSC render can continue indefinitely and a later failure can still reach onRequestError. Fresh evidence in the current tree is the internal rscStream.tee() whose ssrStream branch is never retained for cleanup; cancel both Flight branches with the branded reason and cover a never-settling Server Component rather than one that resolves after 1.5 seconds.

Useful? React with 👍 / 👎.

Comment thread packages/vinext/src/server/response-aborted.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff7605e38e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

body: ReadableStream,
options: BuildAppPageHtmlResponseOptions,
): Response {
body = tagConsumerCancellation(body);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Tag cancellation on boundary-render responses

Apply the cancellation wrapper to the HTTP-access/error-boundary response paths as well. This only tags bodies constructed through buildAppPageRscResponse and buildAppPageHtmlResponse, while renderAppPageBoundaryResponse still returns its RSC stream directly and its HTML branch uses renderAppPageHtmlResponse, which also constructs an untagged Response. If a client disconnects while a suspended custom not-found, forbidden, unauthorized, or error boundary is streaming, the cancellation therefore reaches the same RSC error handler without a ResponseAbortedError and is still reported through onRequestError; the new integration coverage exercises only a normal page.

Useful? React with 👍 / 👎.

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.

onRequestError receives React render-abort errors when the client disconnects mid-stream

1 participant