-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
fix: AbortError in renderToReadableStream #6539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix: AbortError in renderToReadableStream #6539
Conversation
📝 WalkthroughWalkthroughThis change addresses uncatchable Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@packages/react-router/src/ssr/renderRouterToStream.tsx`:
- Around line 29-48: The onError handler currently only logs errors so
render-time errors still yield router.state.statusCode (often 200); modify the
onError passed to the render stream to set a local boolean flag (e.g.,
renderError = true) when any non-abort error occurs, then after creating
responseStream and before returning new Response use that flag to override the
status to 500 (instead of router.state.statusCode) when true; update references
around the existing onError callback, the stream variable used with
stream.allReady/isbot, and the new Response call that currently uses
router.state.statusCode and responseHeaders so the response reflects render
failures.
🧹 Nitpick comments (1)
packages/react-router/src/ssr/renderRouterToStream.tsx (1)
42-47: Properly typetransformReadableStreamWithRouterandtransformStreamWithRouterto eliminate unsafe casts.The functions currently accept and return unparameterized
ReadableStream, forcing call sites to useas unknown as ReadableStreamandas anycasts to match actual usage. Update both function signatures inpackages/router-core/src/ssr/transformStreamWithRouter.tsto explicitly type with<Uint8Array>:
- Line 6-11:
transformReadableStreamWithRouter(router: AnyRouter, routerStream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array>- Line 100-109:
transformStreamWithRouter(router: AnyRouter, appStream: ReadableStream<Uint8Array>, opts?: {...}): ReadableStream<Uint8Array>This matches the implementation (which creates
new ReadableStream<Uint8Array>) and enables strict typing at all call sites (react-router, solid-router, vue-router) without casts.
| onError: (error) => { | ||
| if (error instanceof DOMException && error.name === 'AbortError') { | ||
| return | ||
| } | ||
|
|
||
| if (isbot(request.headers.get('User-Agent'))) { | ||
| await stream.allReady | ||
| } | ||
| console.error('Error in renderToReadableStream:', error) | ||
| }, | ||
| }) | ||
|
|
||
| const responseStream = transformReadableStreamWithRouter( | ||
| router, | ||
| stream as unknown as ReadableStream, | ||
| ) | ||
| return new Response(responseStream as any, { | ||
| status: router.state.statusCode, | ||
| headers: responseHeaders, | ||
| }) | ||
| if (isbot(request.headers.get('User-Agent'))) { | ||
| await stream.allReady | ||
| } | ||
|
|
||
| const responseStream = transformReadableStreamWithRouter( | ||
| router, | ||
| stream as unknown as ReadableStream, | ||
| ) | ||
| return new Response(responseStream as any, { | ||
| status: router.state.statusCode, | ||
| headers: responseHeaders, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the full context of the renderRouterToStream.tsx file
fd renderRouterToStream.tsx -t fRepository: TanStack/router
Length of output: 221
🏁 Script executed:
# Once we have the file path, let's examine the relevant section
cat -n packages/react-router/src/ssr/renderRouterToStream.tsx | head -100Repository: TanStack/router
Length of output: 3920
🏁 Script executed:
# Search for other SSR error handling patterns
rg "onError|didError" packages/react-router/src/ssr -A 3 -B 3Repository: TanStack/router
Length of output: 1394
🏁 Script executed:
# Check if there are any tests for renderRouterToStream error handling
fd -e test.ts -e test.tsx -e spec.ts -e spec.tsx | xargs grep -l "renderRouterToStream\|onError" 2>/dev/null | head -5Repository: TanStack/router
Length of output: 292
🏁 Script executed:
# Let's check if there are any related tests or documentation about error handling
rg "renderToReadableStream.*onError" -A 10 -B 5 packages/Repository: TanStack/router
Length of output: 41
🏁 Script executed:
# Check git history to see if there were recent changes related to error handling
git log --oneline packages/react-router/src/ssr/renderRouterToStream.tsx | head -20Repository: TanStack/router
Length of output: 110
🏁 Script executed:
# Let's look for any other error handling patterns in the codebase
rg "didError|errorBoundary" packages/react-router -A 2 -B 2Repository: TanStack/router
Length of output: 41
🏁 Script executed:
# Let's check the git commit that fixed AbortError to understand the error handling approach
git show 90c04a311 --statRepository: TanStack/router
Length of output: 50372
🏁 Script executed:
# Look at the commit to understand the context
git show 90c04a311Repository: TanStack/router
Length of output: 50376
🌐 Web query:
React renderToReadableStream onError callback behavior when to return status 500
💡 Result:
renderToReadableStream’s onError fires for any server rendering error (recoverable or not), including errors that happen inside <Suspense> boundaries where React can still stream a shell and let the client recover. In those “recoverable” cases, onError runs but the renderToReadableStream(...) promise does not reject. (react.dev)
When to return HTTP 500
1) Shell (initial HTML) fails → return 500 via catch
- If React can’t produce a meaningful shell,
await renderToReadableStream(...)will throw/reject, so you can return a fallback HTML response withstatus: 500. - In this case, both
onErrorand yourcatchrun (useonErrorfor logging; usecatchto construct the 500 response). (react.dev)
2) Non-shell (inside <Suspense>) errors → you may still return 200
- React will keep streaming (often sending the fallback) and try to recover on the client, so many apps choose 200 even though
onErrorfired. (react.dev)
3) If you want 500 when any error happened before you create the Response
Track a flag in onError, then set status when returning the Response (works only for errors that occur before headers are committed—i.e., before you return the Response). (react.dev)
let didError = false;
try {
const stream = await renderToReadableStream(<App />, {
onError(err) {
didError = true;
console.error(err);
},
});
return new Response(stream, {
status: didError ? 500 : 200,
headers: { "content-type": "text/html" },
});
} catch (err) {
return new Response("<h1>Something went wrong</h1>", {
status: 500,
headers: { "content-type": "text/html" },
});
}Key streaming constraint
Once streaming starts (i.e., once you’ve returned the Response and bytes may be sent), you generally can’t change the status code afterward—so decide the status before returning the Response. (react.dev)
Citations:
- 1: https://react.dev/reference/react-dom/server/renderToReadableStream?utm_source=openai
- 2: https://react.dev/reference/react-dom/server/renderToReadableStream?utm_source=openai
- 3: https://react.dev/reference/react-dom/server/renderToReadableStream?utm_source=openai
- 4: https://react.dev/reference/react-dom/server/renderToReadableStream?utm_source=openai
- 5: https://react.dev/reference/react-dom/server/renderToReadableStream?utm_source=openai
Prevent HTTP 200 responses when render errors occur during streaming.
The onError callback logs rendering errors but doesn't affect the response status—render-time errors within <Suspense> boundaries won't reject the stream promise, so the response still uses router.state.statusCode (likely 200). Track errors in onError and return 500 when they occur:
🐛 Proposed fix
- try {
- const stream = await ReactDOMServer.renderToReadableStream(children, {
+ try {
+ let didError = false
+ const stream = await ReactDOMServer.renderToReadableStream(children, {
signal: request.signal,
nonce: router.options.ssr?.nonce,
progressiveChunkSize: Number.POSITIVE_INFINITY,
onError: (error) => {
if (error instanceof DOMException && error.name === 'AbortError') {
return
}
- console.error('Error in renderToReadableStream:', error)
+ didError = true
+ console.error('Error in renderToReadableStream:', error)
},
})
- return new Response(responseStream as any, {
- status: router.state.statusCode,
+ return new Response(responseStream as any, {
+ status: didError ? 500 : router.state.statusCode,
headers: responseHeaders,
})🤖 Prompt for AI Agents
In `@packages/react-router/src/ssr/renderRouterToStream.tsx` around lines 29 - 48,
The onError handler currently only logs errors so render-time errors still yield
router.state.statusCode (often 200); modify the onError passed to the render
stream to set a local boolean flag (e.g., renderError = true) when any non-abort
error occurs, then after creating responseStream and before returning new
Response use that flag to override the status to 500 (instead of
router.state.statusCode) when true; update references around the existing
onError callback, the stream variable used with stream.allReady/isbot, and the
new Response call that currently uses router.state.statusCode and
responseHeaders so the response reflects render failures.
Fixes #6069
When running TanStack Start in production I was getting hundreds of
DOMException: The connection was closederrors.This checks during
renderToReadableStreamif an error has occurred. Since this is a non-recoverable error, a499HTTP response is sent instead. I'm not sure if this is the best approach and welcome any feedback.I've tested it using this reproduction (https://github.com/checkerschaf/start-domexception-aborterror/blob/main/reproduce-abort-error.ts). After this fix, no errors are logged.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.