-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
onError in renderRouterToStream destroys stream even for recoverable errors, breaking Error Boundary SSR rendering #7078
Description
Which project does this relate to?
Start
Describe the bug
In renderRouterToStream.tsx, the onError callback added in #5996 destroys the PassThrough stream on any error, including errors that are caught by React Error Boundaries (recoverable errors).
React's renderToPipeableStream calls onError for all errors — both recoverable (caught by Error Boundary) and unrecoverable. When the error is recoverable:
- Error Boundary catches the error and renders
errorComponent/defaultErrorComponent onShellReadyfires normally — the shell includes the error fallback UIonErroralso fires →stream.destroy()kills the already-piping stream- Response body becomes broken → upstream proxy returns 502 Bad Gateway
This means defaultErrorComponent and route-level errorComponent never render during SSR — users always see a 502 instead of the intended error page.
Your Example Website or App
N/A (reproducible in any TanStack Start app with defaultErrorComponent set)
Steps to Reproduce the Bug or Issue
- Set
defaultErrorComponentincreateRouter():
const router = createRouter({
routeTree,
defaultErrorComponent: () => <MyErrorPage />,
})- Throw an error in any route component:
function RouteComponent() {
throw new Error('test error')
return <div>Hello</div>
}- Access the page via SSR → 502 Bad Gateway instead of MyErrorPage
Expected behavior
The Error Boundary should catch the error and render defaultErrorComponent during SSR, returning a valid HTML response (with status 500).
Screenshots or Videos
No response
Platform
@tanstack/react-router: 1.167.16 (any version after #5996 merge on 2025-11-29)
@tanstack/react-start: 1.167.16
Additional context
Root Cause
In packages/react-router/src/ssr/renderRouterToStream.tsx:
onError: (error, info) => {
console.error('Error in renderToPipeableStream:', error, info)
// Destroy the passthrough stream on error
if (!reactAppPassthrough.destroyed) {
reactAppPassthrough.destroy(
error instanceof Error ? error : new Error(String(error)),
)
}
},Proposed Fix
Follow React's recommended pattern:
onShellError(error) {
console.error('SSR Shell Error:', error)
if (!reactAppPassthrough.destroyed)
reactAppPassthrough.destroy(
error instanceof Error ? error : new Error(String(error)),
)
},
onError: (error, info) => {
console.error('Error in renderToPipeableStream:', error, info)
},onError: logging/flag-setting only, nostream.destroy().
This aligns with React's recommended pattern
whereonErroris used solely for logging — none of React's official
examples destroy the stream inonError.onShellError(new): handle unrecoverable shell errors with fallback HTML,
as documented in React's API reference.