Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,8 @@ async function generateDynamicFlightRenderResultWithStagesInDev(
req: BaseNextRequest,
ctx: AppRenderContext,
initialRequestStore: RequestStore,
createRequestStore: (() => RequestStore) | undefined
createRequestStore: (() => RequestStore) | undefined,
tree: LoaderTree
): Promise<RenderResult> {
const {
htmlRequestId,
Expand Down Expand Up @@ -746,6 +747,8 @@ async function generateDynamicFlightRenderResultWithStagesInDev(
onFlightDataRenderError
)

const [resolveValidation, validationOutlet] = createValidationOutlet()

const getPayload = async (requestStore: RequestStore) => {
const payload: RSCPayload & RSCPayloadDevProperties =
await workUnitAsyncStorage.run(
Expand All @@ -763,6 +766,8 @@ async function generateDynamicFlightRenderResultWithStagesInDev(
})
}

payload._validation = validationOutlet

return payload
}

Expand Down Expand Up @@ -822,6 +827,24 @@ async function generateDynamicFlightRenderResultWithStagesInDev(
setReactDebugChannel(debugChannel.clientSide, htmlRequestId, requestId)
}

const devValidatingFallbackParams =
getRequestMeta(req, 'devValidatingFallbackParams') || null

// TODO(restart-on-cache-miss):
// This can probably be optimized to do less work,
// because we've already made sure that we have warm caches.
consoleAsyncStorage.run(
{ dim: true },
spawnDynamicValidationInDev,
resolveValidation,
tree,
ctx,
false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
false,
ctx.res.statusCode === 404,

The code passes false unconditionally as the isNotFound parameter to spawnDynamicValidationInDev, but it should check if the response status code is 404 like the existing code does at line 2578.

View Details

Analysis

Incorrect hardcoded false for isNotFound parameter in generateDynamicFlightRenderResultWithStagesInDev

What fails: generateDynamicFlightRenderResultWithStagesInDev at line 842 passes false unconditionally to spawnDynamicValidationInDev, but should check ctx.res.statusCode === 404 like the call at line 2578 does. This causes incorrect metadata and validation error handling for 404 pages during development HMR validation.

How to reproduce:

  1. Create a Next.js app with a custom 404 page (app/not-found.js or pages/404.js)
  2. Run in development mode with NODE_ENV=development and cache components enabled
  3. Perform HMR refresh/validation on a 404 page request (RSC request to the 404 route)
  4. The validation outlet receives incorrect isNotFound=false instead of isNotFound=true

Expected vs actual behavior:

  • The isNotFound parameter should reflect whether the response is a 404 (i.e., res.statusCode === 404)
  • This determines whether metadata error type is set to 'not-found' in getRSCPayload (line 1278), which affects how validation errors are reported for 404 pages
  • Current code: passes hardcoded false, causing validation to treat 404 pages as normal pages
  • Expected: passes ctx.res.statusCode === 404, matching the pattern used at line 2578 in renderToStream

Pattern consistency: The response object ctx.res is initialized with statusCode = 404 at line 1651-1652 for 404 page paths and is included in AppRenderContext. Both call sites to spawnDynamicValidationInDev should use the same logic.

Fix: Change line 842 from false, to ctx.res.statusCode === 404,

ctx.clientReferenceManifest,
initialRequestStore,
devValidatingFallbackParams
)

return new FlightRenderResult(stream, {
fetchMetrics: workStore.fetchMetrics,
})
Expand Down Expand Up @@ -2016,7 +2039,8 @@ async function renderToHTMLOrFlightImpl(
req,
ctx,
requestStore,
createRequestStore
createRequestStore,
loaderTree
)
} else {
return generateDynamicFlightRenderResult(req, ctx, requestStore)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import stripAnsi from 'strip-ansi'
import { nextTestSetup } from 'e2e-utils'
import {
assertNoRedbox,
assertNoErrorToast,
hasErrorToast,
retry,
} from 'next-test-utils'
import { assertNoRedbox, hasErrorToast, retry } from 'next-test-utils'
import { createSandbox } from 'development-sandbox'
import { outdent } from 'outdent'

Expand Down Expand Up @@ -37,18 +32,32 @@ describe('Cache Components Dev Errors', () => {
`)
})

it('should not show a red box error on client navigations', async () => {
it('should show a red box error on client navigations', async () => {
const browser = await next.browser('/no-error')

await retry(async () => {
expect(await hasErrorToast(browser)).toBe(false)
})

await browser.elementByCss("[href='/error']").click()
await assertNoErrorToast(browser)
// TODO: React should not include the anon stack in the Owner Stack.
await expect(browser).toDisplayCollapsedRedbox(`
{
"description": "Route "/error" used \`Math.random()\` before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing random values synchronously in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-random",
"environmentLabel": "Server",
"label": "Console Error",
"source": "app/error/page.tsx (2:23) @ Page
> 2 | const random = Math.random()
| ^",
"stack": [
"Page app/error/page.tsx (2:23)",
"Page <anonymous>",
"LogSafely <anonymous>",
],
}
`)

await browser.loadPage(`${next.url}/error`)

// TODO: React should not include the anon stack in the Owner Stack.
await expect(browser).toDisplayCollapsedRedbox(`
{
Expand Down
Loading