Skip to content

Conversation

@Rani367
Copy link

@Rani367 Rani367 commented Dec 10, 2025

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:

  1. Check reactServerErrorsByDigest for any errors with the HTTP_ERROR_FALLBACK_ERROR_CODE prefix
  2. If no such errors exist, send the buffered RSC data as before
  3. 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: Tests notFound() thrown inside a Suspense boundary
  • not-found-with-layout-suspense: Exact reproduction of the issue - notFound() in page with async component in layout's Suspense

Fixes #86251

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Dec 10, 2025

Allow CI Workflow Run

  • approve CI run for commit: 5f3bc67

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

@harikapadia999
Copy link

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:

  1. Performance consideration: Buffering the entire RSC stream in memory (chunks array) could be memory-intensive for large responses. Consider adding a comment about this trade-off or exploring streaming detection if possible.

  2. 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).

  3. 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! 🚀

@dsbrianwebster
Copy link

dsbrianwebster commented Dec 29, 2025

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 />
}

@Rani367
Copy link
Author

Rani367 commented Dec 29, 2025

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 cacheComponents with notFound(). Happy to address any feedback. Thanks!

@Rani367 Rani367 force-pushed the fix/notfound-suspense-cache-components branch from 5f3bc67 to ec2b374 Compare January 5, 2026 07:33
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 vercel#86251
@Rani367 Rani367 force-pushed the fix/notfound-suspense-cache-components branch from ec2b374 to 8a7df5b Compare January 5, 2026 08:17
@Rani367
Copy link
Author

Rani367 commented Jan 5, 2026

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

notFound breaks Suspense in layout with cacheComponents enabled

4 participants