Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@checkerschaf
Copy link

@checkerschaf checkerschaf commented Jan 28, 2026

Fixes #6069

When running TanStack Start in production I was getting hundreds of DOMException: The connection was closed errors.

This checks during renderToReadableStream if an error has occurred. Since this is a non-recoverable error, a 499 HTTP 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

  • Bug Fixes
    • Improved error handling for server-side rendering stream operations with proper HTTP status codes (499 for aborted requests, 500 for unexpected errors)
    • Enhanced error logging for better diagnostics while gracefully handling aborted requests

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

This change addresses uncatchable DOMException: The connection was closed errors during server-side streaming by wrapping the renderToReadableStream flow in error handling. It now catches and logs errors appropriately, returning 499 on stream abort and 500 on other unexpected errors.

Changes

Cohort / File(s) Summary
Stream Error Handling
packages/react-router/src/ssr/renderRouterToStream.tsx
Wrapped renderToReadableStream in try/catch block with onError handler. Suppresses AbortError silently, logs other errors, and returns HTTP 499 for aborted streams or 500 for unexpected failures. Existing bot-readiness behavior preserved.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • fix: more robust stream handling #5996: Related stream error handling improvements in the same file, specifically addressing pipeable-stream error destruction alongside this readable-stream error handling.

Poem

🐰 A hop and a skip through errors we go,
Catching those aborts with a graceful flow,
No more connection closes in the logs,
Just silent returns through the streaming bogs,
Streams now finish their work, no more fogs! 🌊

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: AbortError in renderToReadableStream' accurately describes the main change - adding error handling for AbortError in the renderToReadableStream function to address #6069.
Linked Issues check ✅ Passed The pull request implements the expected behavior from #6069 by wrapping renderToReadableStream in try/catch, detecting AbortError, returning 499 for aborted streams, and preventing unhandled errors from flooding logs.
Out of Scope Changes check ✅ Passed All changes are focused on fixing the AbortError handling in renderToReadableStream. No unrelated modifications to renderToPipeableStream path or other out-of-scope code were introduced.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 type transformReadableStreamWithRouter and transformStreamWithRouter to eliminate unsafe casts.

The functions currently accept and return unparameterized ReadableStream, forcing call sites to use as unknown as ReadableStream and as any casts to match actual usage. Update both function signatures in packages/router-core/src/ssr/transformStreamWithRouter.ts to 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.

Comment on lines +29 to +48
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,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the full context of the renderRouterToStream.tsx file
fd renderRouterToStream.tsx -t f

Repository: 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 -100

Repository: 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 3

Repository: 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 -5

Repository: 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 -20

Repository: 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 2

Repository: 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 --stat

Repository: TanStack/router

Length of output: 50372


🏁 Script executed:

# Look at the commit to understand the context
git show 90c04a311

Repository: 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 with status: 500.
  • In this case, both onError and your catch run (use onError for logging; use catch to 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 onError fired. (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:


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.

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.

DOMException: The connection was closed is not catchable

1 participant