fix: handle notFound in Suspense with cacheComponents enabled
What?
This PR fixes an issue where notFound() (and other HTTP access fallback errors like forbidden()/unauthorized()) would cause "Connection closed" errors when cacheComponents: true is enabled and the page has a Suspense boundary with an async component.
Why?
When cacheComponents is enabled and a page is prerendered, subsequent requests use DynamicState.DATA mode which only sends RSC data without re-rendering the HTML shell. However, when notFound() is thrown during this RSC render, the prerendered HTML doesn't contain the not-found component, causing the client to receive an incomplete stream and display "Connection closed" errors.
How?
This fix buffers the RSC stream in DynamicState.DATA mode to detect HTTP access fallback errors. After consuming the stream:
- Check
reactServerErrorsByDigestfor any errors with theHTTP_ERROR_FALLBACK_ERROR_CODEprefix - If no such errors exist, send the buffered RSC data as before
- If such errors are found, set the appropriate status code and fall through to the full dynamic render path which properly handles the not-found page
Test Plan
Added two test cases:
not-found-suspense: TestsnotFound()thrown inside a Suspense boundarynot-found-with-layout-suspense: Exact reproduction of the issue -notFound()in page with async component in layout's Suspense
Fixes #86251
Allow CI Workflow Run
- [ ] approve CI run for commit: 5f3bc6798896e03e668bfb620ca95f76a810a805
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer
Excellent fix for a tricky edge case! 🎯
Problem Identified:
When cacheComponents: true is enabled and notFound() is thrown inside a Suspense boundary, the RSC stream was being sent directly without detecting the HTTP access fallback error, causing "Connection closed" errors on the client.
Solution Approach:
Your fix properly consumes the entire RSC stream first to detect HTTP access fallback errors (like notFound()), then:
- If no errors → sends the buffered RSC data as before
- If HTTP error detected → falls through to full dynamic render to properly display the error page
Strengths:
✅ Comprehensive test coverage with two test cases covering different scenarios
✅ Proper error detection by checking reactServerErrorsByDigest
✅ Maintains backward compatibility for non-error cases
✅ Sets correct HTTP status codes
✅ Clean fallback to dynamic rendering when needed
Minor Suggestions:
-
Performance consideration: Buffering the entire RSC stream in memory (
chunksarray) could be memory-intensive for large responses. Consider adding a comment about this trade-off or exploring streaming detection if possible. -
Code comment clarity: The comment "We need to consume the RSC stream first..." is great, but could also mention why we can't detect this during streaming (because Suspense boundaries can catch errors).
-
Test robustness: Consider adding a test case for a large page to ensure the buffering approach doesn't cause memory issues.
Overall, this is a solid fix for a complex streaming + error handling interaction! 🚀