fix: handle notFound in Suspense with cacheComponents enabled#87041
fix: handle notFound in Suspense with cacheComponents enabled#87041Rani367 wants to merge 1 commit into
Conversation
|
Allow CI Workflow Run
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: Solution Approach:
Strengths: Minor Suggestions:
Overall, this is a solid fix for a complex streaming + error handling interaction! 🚀 |
|
We are experiencing this and it is pretty much a deal killer for being able to use cache components as it essentially means we can have any working 404 behavior on pages. Hoping @Rani367's fix makes it into the next release soon. import { notFound } from 'next/navigation'
export default async function ArticlePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const article = await findArticle(slug)
if (!article) {
notFound() // results in connection closed and error page instead of 404
}
return <article />
} |
|
Hi @gnoff, could you take a look at this PR when you have a chance? There are users blocked by this issue (see #86251 and the comment above from @dsbrianwebster) who can't use |
5f3bc67 to
ec2b374
Compare
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() (or forbidden/ unauthorized) is thrown during this RSC render, the prerendered HTML doesn't contain the not-found component, causing Connection closed errors. This fix buffers the RSC stream in DATA mode to detect HTTP access fallback errors. If such errors are found, it falls through to the full dynamic render path which properly handles the not-found page. Fixes #86251
ec2b374 to
8a7df5b
Compare
|
Updated the fix to also store HTTP access fallback errors in reactServerErrorsByDigest (in create-error-handler.tsx), which is needed for the detection logic in app-render.tsx to work correctly. |
|
@Rani367 thanks for the updates. I've been communicating about this to Vercel through support ticket for an upcoming project. Hopefully that helps pull in some senior eyes from the NextJS team on this. Broken 404 behavior is a pretty critical concern, especially for SEO. IMO, if this is a known limitation of cache components in their current form, this really feels like a broader, production-blocking issue, not just for us, but for other teams evaluating cache components for real-world deployments, and one deserving of some priority attention, or a return to an experimental flag disclaimer to opt in to cache components. 🤞 For this to get reviewed and merged soon. |
| // We need to consume the RSC stream first to check for HTTP access fallback | ||
| // errors (like notFound()). If such an error is thrown inside a Suspense | ||
| // boundary, we can't just send RSC data - we need to do a full dynamic | ||
| // render to properly display the error page. | ||
| const rscStream = reactServerResult.consume() | ||
| const reader = rscStream.getReader() | ||
| const chunks: Uint8Array[] = [] | ||
|
|
||
| // Consume the entire RSC stream to detect any errors | ||
| try { | ||
| while (true) { | ||
| const { done, value } = await reader.read() | ||
| if (done) break | ||
| if (value) chunks.push(value) | ||
| } | ||
| } finally { | ||
| reader.releaseLock() | ||
| } |
There was a problem hiding this comment.
We cannot consume the entire stream here in this situation, as it prevents streaming the response to the client. I'll investigate the connection closed error, as that should not occur here, and it should instead trigger the not found boundary added by the framework which is responsible for rendering the boundary client side instead.
There was a problem hiding this comment.
Thanks for the review @wyattjoh! I understand the concern about buffering breaking streaming.
I couldn't find another way to detect these errors without consuming the stream first, but I'd be happy to explore alternative approaches. Looking forward to the results of your investigation into why the not-found boundary isn't triggering client-side in this case.
Let me know if there's anything I can help with or if you'd like me to try a different approach.
There was a problem hiding this comment.
I'm experimenting with a few approaches to solve, I'll follow up here as soon as I got some more details for you! Been experimenting over at #88444 if you'd like to follow the experience!
|
Any updates on this? |
|
…#92231) When a `notFound()`, `forbidden()`, or `unauthorized()` error escapes into the outer prerender recovery path, we were falling back to the generic error shell flow. In the `cacheComponents` case, that could leave us with: - error HTML rendered from `ErrorApp` - Flight data reused from the aborted prerender prelude - references to Flight chunks that were never emitted That is what caused the client-side `Connection closed` failure in vercel#86251. Instead of rerendering the full Flight tree or always using the generic error RSC payload, this change: - finds the deepest matching HTTP fallback boundary - rerenders the normal app router payload with that segment-scoped fallback - tees the replacement Flight stream so Fizz can render from one copy while prerender buffering consumes the other - only takes this path for recoverable HTTP access fallbacks that have a real boundary (ie a defined not-found/unauthorized/etc) If no matching boundary exists, we keep the existing generic error handling. Fixes vercel#86251 Fixes vercel#90837 Closes vercel#87041 Closes vercel#86251 Closes NAR-711 Closes NEXT-4876
What?
This PR fixes an issue where
notFound()(and other HTTP access fallback errors likeforbidden()/unauthorized()) would cause "Connection closed" errors whencacheComponents: trueis enabled and the page has a Suspense boundary with an async component.Why?
When
cacheComponentsis enabled and a page is prerendered, subsequent requests useDynamicState.DATAmode which only sends RSC data without re-rendering the HTML shell. However, whennotFound()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.DATAmode to detect HTTP access fallback errors. After consuming the stream:reactServerErrorsByDigestfor any errors with theHTTP_ERROR_FALLBACK_ERROR_CODEprefixTest 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 SuspenseFixes #86251