fix(rsc): handle router errors gracefully in flight response stream#88444
fix(rsc): handle router errors gracefully in flight response stream#88444wyattjoh wants to merge 5 commits into
Conversation
Stats from current PR🟢 1 improvement
📊 All Metrics📖 Metrics GlossaryDev Server Metrics:
Build Metrics:
Change Thresholds:
⚡ Dev Server
📦 Dev Server (Webpack) (Legacy)📦 Dev Server (Webpack)
⚡ Production Builds
📦 Production Builds (Webpack) (Legacy)📦 Production Builds (Webpack)
📦 Bundle SizesBundle Sizes⚡ TurbopackClient Main Bundles: **430 kB** → **431 kB**
|
| Canary | PR | Change | |
|---|---|---|---|
| middleware-b..fest.js gzip | 790 B | 789 B | ✓ |
| Total | 790 B | 789 B | ✅ -1 B |
Build Details
Build Manifests
| Canary | PR | Change | |
|---|---|---|---|
| _buildManifest.js gzip | 452 B | 452 B | ✓ |
| Total | 452 B | 452 B | ✓ |
📦 Webpack
Client
Main Bundles
| Canary | PR | Change | |
|---|---|---|---|
| 2086.HASH.js gzip | 169 B | N/A | - |
| 2161-HASH.js gzip | 5.41 kB | N/A | - |
| 2747-HASH.js gzip | 4.48 kB | N/A | - |
| 4322-HASH.js gzip | 52.3 kB | N/A | - |
| ec793fe8-HASH.js gzip | 62.3 kB | N/A | - |
| framework-HASH.js gzip | 59.8 kB | 59.8 kB | ✓ |
| main-app-HASH.js gzip | 252 B | 254 B | ✓ |
| main-HASH.js gzip | 38.6 kB | 39 kB | 🔴 +424 B (+1%) |
| webpack-HASH.js gzip | 1.68 kB | 1.68 kB | ✓ |
| 1596.HASH.js gzip | N/A | 169 B | - |
| 2658-HASH.js gzip | N/A | 52.7 kB | - |
| 6349-HASH.js gzip | N/A | 4.46 kB | - |
| 7019-HASH.js gzip | N/A | 5.43 kB | - |
| b17a3386-HASH.js gzip | N/A | 62.3 kB | - |
| Total | 225 kB | 226 kB |
Polyfills
| Canary | PR | Change | |
|---|---|---|---|
| polyfills-HASH.js gzip | 39.4 kB | 39.4 kB | ✓ |
| Total | 39.4 kB | 39.4 kB | ✓ |
Pages
| Canary | PR | Change | |
|---|---|---|---|
| _app-HASH.js gzip | 194 B | 193 B | ✓ |
| _error-HASH.js gzip | 182 B | 182 B | ✓ |
| css-HASH.js gzip | 336 B | 335 B | ✓ |
| dynamic-HASH.js gzip | 1.8 kB | 1.8 kB | ✓ |
| edge-ssr-HASH.js gzip | 256 B | 256 B | ✓ |
| head-HASH.js gzip | 352 B | 349 B | ✓ |
| hooks-HASH.js gzip | 385 B | 384 B | ✓ |
| image-HASH.js gzip | 580 B | 580 B | ✓ |
| index-HASH.js gzip | 259 B | 258 B | ✓ |
| link-HASH.js gzip | 2.5 kB | 2.51 kB | ✓ |
| routerDirect..HASH.js gzip | 319 B | 317 B | ✓ |
| script-HASH.js gzip | 385 B | 387 B | ✓ |
| withRouter-HASH.js gzip | 316 B | 315 B | ✓ |
| 1afbb74e6ecf..834.css gzip | 106 B | 106 B | ✓ |
| Total | 7.97 kB | 7.96 kB | ✅ -8 B |
Server
Edge SSR
| Canary | PR | Change | |
|---|---|---|---|
| edge-ssr.js gzip | 125 kB | 125 kB | ✓ |
| page.js gzip | 242 kB | 242 kB | ✓ |
| Total | 367 kB | 367 kB |
Middleware
| Canary | PR | Change | |
|---|---|---|---|
| middleware-b..fest.js gzip | 654 B | 653 B | ✓ |
| middleware-r..fest.js gzip | 155 B | 156 B | ✓ |
| middleware.js gzip | 33.2 kB | 33.3 kB | ✓ |
| edge-runtime..pack.js gzip | 842 B | 842 B | ✓ |
| Total | 34.9 kB | 34.9 kB |
Build Details
Build Manifests
| Canary | PR | Change | |
|---|---|---|---|
| _buildManifest.js gzip | 738 B | 738 B | ✓ |
| Total | 738 B | 738 B | ✓ |
Build Cache
| Canary | PR | Change | |
|---|---|---|---|
| 0.pack gzip | 3.66 MB | 3.67 MB | 🔴 +7.67 kB (+0%) |
| index.pack gzip | 100 kB | 99 kB | 🟢 1.2 kB (-1%) |
| index.pack.old gzip | 99.7 kB | 100 kB | ✓ |
| Total | 3.86 MB | 3.87 MB |
🔄 Shared (bundler-independent)
Runtimes
| Canary | PR | Change | |
|---|---|---|---|
| app-page-exp...dev.js gzip | 304 kB | 304 kB | ✓ |
| app-page-exp..prod.js gzip | 158 kB | 159 kB | ✓ |
| app-page-tur...dev.js gzip | 304 kB | 304 kB | ✓ |
| app-page-tur..prod.js gzip | 158 kB | 159 kB | ✓ |
| app-page-tur...dev.js gzip | 300 kB | 301 kB | ✓ |
| app-page-tur..prod.js gzip | 156 kB | 157 kB | ✓ |
| app-page.run...dev.js gzip | 300 kB | 301 kB | ✓ |
| app-page.run..prod.js gzip | 156 kB | 157 kB | ✓ |
| app-route-ex...dev.js gzip | 68.8 kB | 68.8 kB | ✓ |
| app-route-ex..prod.js gzip | 47.6 kB | 47.6 kB | ✓ |
| app-route-tu...dev.js gzip | 68.8 kB | 68.8 kB | ✓ |
| app-route-tu..prod.js gzip | 47.6 kB | 47.6 kB | ✓ |
| app-route-tu...dev.js gzip | 68.4 kB | 68.4 kB | ✓ |
| app-route-tu..prod.js gzip | 47.4 kB | 47.4 kB | ✓ |
| app-route.ru...dev.js gzip | 68.4 kB | 68.4 kB | ✓ |
| app-route.ru..prod.js gzip | 47.4 kB | 47.3 kB | ✓ |
| dist_client_...dev.js gzip | 324 B | 324 B | ✓ |
| dist_client_...dev.js gzip | 326 B | 326 B | ✓ |
| dist_client_...dev.js gzip | 318 B | 318 B | ✓ |
| dist_client_...dev.js gzip | 317 B | 317 B | ✓ |
| pages-api-tu...dev.js gzip | 41.2 kB | 41.2 kB | ✓ |
| pages-api-tu..prod.js gzip | 31.3 kB | 31.3 kB | ✓ |
| pages-api.ru...dev.js gzip | 41.1 kB | 41.1 kB | ✓ |
| pages-api.ru..prod.js gzip | 31.2 kB | 31.2 kB | ✓ |
| pages-turbo....dev.js gzip | 50.8 kB | 50.8 kB | ✓ |
| pages-turbo...prod.js gzip | 38.2 kB | 38.2 kB | ✓ |
| pages.runtim...dev.js gzip | 50.7 kB | 50.7 kB | ✓ |
| pages.runtim..prod.js gzip | 38.2 kB | 38.2 kB | ✓ |
| server.runti..prod.js gzip | 62.2 kB | 62.2 kB | ✓ |
| Total | 2.69 MB | 2.69 MB |
📝 Changed Files (13 files)
Files with changes:
app-page-exp..ntime.dev.jsapp-page-exp..time.prod.jsapp-page-tur..ntime.dev.jsapp-page-tur..time.prod.jsapp-page-tur..ntime.dev.jsapp-page-tur..time.prod.jsapp-page.runtime.dev.jsapp-page.runtime.prod.jsapp-route-ex..time.prod.jsapp-route-tu..time.prod.jsapp-route-tu..time.prod.jsapp-route.ru..time.prod.jsserver.runtime.prod.js
View diffs
app-page-exp..ntime.dev.js
failed to diffapp-page-exp..time.prod.js
failed to diffapp-page-tur..ntime.dev.js
failed to diffapp-page-tur..time.prod.js
failed to diffapp-page-tur..ntime.dev.js
failed to diffapp-page-tur..time.prod.js
Diff too large to display
app-page.runtime.dev.js
failed to diffapp-page.runtime.prod.js
Diff too large to display
app-route-ex..time.prod.js
Diff too large to display
app-route-tu..time.prod.js
Diff too large to display
app-route-tu..time.prod.js
Diff too large to display
app-route.ru..time.prod.js
Diff too large to display
server.runtime.prod.js
Diff too large to display
ca02a5d to
d196360
Compare
This stack of pull requests is managed by Graphite. Learn more about stacking. |
Failing test suitesCommit: afee814 | About building and testing Next.js
Expand output● app-dir parallel-routes-static › should static generate parallel routes
Expand output● use-cache › should prerender fully cacheable pages as static HTML |
d196360 to
306fd3e
Compare
When notFound(), redirect(), forbidden(), or unauthorized() is called inside a Suspense boundary, the stream should close gracefully instead of erroring. This allows the serialized error data to reach the client where error boundaries can handle them properly. Previously, these router errors would cause "Connection closed" errors because the stream was erroring instead of closing cleanly.
- Add isNextRouterError check to createFlightDataInjectionTransformStream - Cancel reader and stop pulling gracefully for router errors - Add reader.cancel() call in use-flight-response.tsx before closing - Skip complex test case (notFound at page level) with TODO explanation
306fd3e to
2d5ea53
Compare
The previous commits applied the new HTTP error re-render path to all cases, which broke non-cacheComponents prerendering (causing 500 errors instead of proper 404 pages). This change: - Adds `cacheComponents &&` guard to only use the new re-render approach when Cache Components is enabled - Removes redundant router error handling from stream utilities since the standard error payload handling works correctly for non-cacheComponents
- Fix hardcoded `is404: true` to use `errorType === 'not-found'` so forbidden (403) and unauthorized (401) errors get their proper metadata instead of incorrectly using not-found metadata - Improve type safety by using `getAccessFallbackHTTPStatus(err)` directly instead of relying on `res.statusCode` being set correctly - Add test coverage for forbidden() and unauthorized() with cacheComponents and Suspense boundaries in layouts
- Delete prerender-error-context.tsx (unused context, no provider) - Remove dead code from error-boundary.tsx (unreachable client check) - Remove unused PrerenderHTTPErrorContext export from entry-base.ts - Add HTTPAccessErrorStatusCode type to http-access-fallback.ts - Simplify fallback selection in create-component-tree.tsx using map lookup - Use shared type in app-render.tsx for cleaner type assertion - Extract expectNoConnectionErrors helper in cache-components.test.ts The server-side approach in createComponentTree substitutes fallback elements directly into the React tree, making the client-side context unnecessary. This simplifies the codebase without affecting functionality.
|
Hey @wyattjoh — following up here since you mentioned in #87041 that you were experimenting with this approach. It looks like this has been in draft with a couple of test failures since mid-January. Is this still something you're planning to land, or has the approach hit a wall? The underlying issue (#86251) has been open for ~4 months now and has 10 upvotes — Happy to help move this forward if there's anything I can pick up — whether that's rebasing, fixing the failing tests, or iterating on the approach. Just let me know. |
|
I don't have any immediate plans to pick this back up, but the approach in this PR could serve as a reasonable starting point for a proper fix. The core idea of using The buffering approach suggested in #87041 would break streaming, so that's not a viable path forward. The fix needs to preserve streaming behavior while still allowing error boundaries to handle I may revisit this when I have some time. If anyone else wants to pick it up in the meantime, the work here should give you a head start on the direction. |

What?
When
notFound(),redirect(),forbidden(), orunauthorized()is called inside a Suspense boundary with cache components enabled, the page would fail with "Connection closed" errors instead of properly rendering the not-found/error page.Why?
The flight response stream was calling
controller.error(error)for all caught errors, including Next.js router errors. This would abort the stream before the serialized error data could reach the client, preventing error boundaries from handling them properly.How?
Check if the caught error is a Next.js router error using
isNextRouterError(). If so, close the stream gracefully withcontroller.close()instead of erroring, allowing the serialized error data to reach the client where error boundaries can handle them.Fixes #86251
NAR-711