cloudflare / cloudflare/vinext

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

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

Description

### Summary

When a client disconnects or aborts navigation while an App Router response is still streaming, the aborted render is reported through `instrumentation.ts` `onRequestError` as a real error: `Error: The render was aborted by the server without a reason.` On a production app with Sentry's `captureRequestError` wired to `onRequestError`, this produces thousands of noise events per day (we measured about 477 in 6 hours, all with response status 200), which drowns out real errors.

Next.js treats consumer aborts as non errors: `pipe-readable.js` checks `isAbortError` (error name `AbortError` or `ResponseAborted`) and returns silently, so client disconnects never reach `onRequestError`.

### Chain (verified against the built server bundle)

1. The client aborts mid stream (navigation away, closed tab, cancelled fetch). The server runtime cancels the response `ReadableStream` with no reason.
2. The cancellation propagates to the Flight stream. Its underlying source calls `abort(request, reason)` with `reason === undefined`.
3. React creates `Error("The render was aborted by the server without a reason.")` for the remaining tasks and invokes the render `onError` with it. The error's creation stack sits inside the webstreams cancel internals (`readableByteStreamControllerCancelSteps -> cancelAlgorithm -> Object.cancel`), which is the signature we see in Sentry.
4. `createRscOnErrorHandler` (`src/server/app-rsc-errors.ts`) does not recognize abort errors: the error has no well known digest, so it falls through to `reportRequestError` and reaches `onRequestError`.

### Reproduction

Against a production build, authenticated page with a render that takes about 1s:

```
# start a navigation and abort it mid render, a few times
curl -m 0.7 https://app.example.com/heavy-page # or navigate away in a browser before the page finishes
```

Each aborted render produces one or more `onRequestError` calls with the message above (React aborts every pending task, so a single abort can emit several events). We reproduced this deterministically in production: three interrupted browser navigations produced six Sentry events within seconds, on the exact URLs visited.

### Suggested fix

Tag consumer cancellation at vinext's response boundary so the render abort carries an identifiable reason, then classify it in the error handlers instead of reporting it:

1. In the stream wrapper that hands the body to the server runtime (for example `sendAppPageResponse` in `src/server/app-page-render.ts`, whose `cancel(reason)` currently forwards `undefined`), cancel upstream with a named error when no reason is given, e.g. `reader.cancel(reason ?? new ResponseAbortedError())`.
2. In `createRscOnErrorHandler` and the SSR `onError`, skip `reportRequestError` when the error is that tagged abort (still returning a digest for React), mirroring Next.js's `isAbortError` handling.

This keeps real render errors fully reported. Only aborts caused by the response consumer going away are reclassified, which matches Next.js semantics.

Happy to send a PR if this direction sounds right.

Contributor guide

Open the contributing guide

Research direction

Start with src/server/app-page-render.ts, especially sendAppPageResponse and its stream cancel path, then trace the abort through src/server/app-rsc-errors.ts and the SSR onError handler. Reproduce the issue with the provided production curl or navigation scenario and compare the handling with Next.js's isAbortError behavior. Done means consumer aborts no longer reach onRequestError, while genuine render errors still do.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.