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

Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 31 additions & 16 deletions packages/react-router/src/ssr/renderRouterToStream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,39 @@ export const renderRouterToStream = async ({
children: ReactNode
}) => {
if (typeof ReactDOMServer.renderToReadableStream === 'function') {
const stream = await ReactDOMServer.renderToReadableStream(children, {
signal: request.signal,
nonce: router.options.ssr?.nonce,
progressiveChunkSize: Number.POSITIVE_INFINITY,
})
try {
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
}

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

})
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') {
return new Response(null, { status: 499, headers: responseHeaders })
}
console.error('Error in renderToReadableStream:', e)
return new Response(null, { status: 500, headers: responseHeaders })
}
}

if (typeof ReactDOMServer.renderToPipeableStream === 'function') {
Expand Down