Skip to content
Draft
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
13 changes: 12 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -1070,5 +1070,16 @@
"1069": "Invariant: cache entry \"%s\" not found in dir \"%s\"",
"1070": "image of size %s could not be tracked by lru cache",
"1071": "toNodeDebugChannel cannot be used in edge/web runtime, this is a bug in the Next.js codebase",
"1072": "Missing segment data: <head>"
"1072": "Missing segment data: <head>",
"1073": "Route \"%s\": %s Except for this instance, the page would have been entirely prerenderable which may have been the intended behavior. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata",
"1074": "Route \"%s\": Uncached data or \\`connection()\\` was accessed inside \\`generateViewport\\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport",
"1075": "Route \"%s\": %s This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/blocking-route",
"1076": "Route \"%s\": Uncached data was accessed outside of <Suspense>. This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/blocking-route",
"1077": "Route \"%s\": Uncached data or \\`connection()\\` was accessed inside \\`generateMetadata\\`. Except for this instance, the page would have been entirely prerenderable which may have been the intended behavior. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata",
"1078": "Route \"%s\": Runtime data such as \\`cookies()\\`, \\`headers()\\`, \\`params\\`, or \\`searchParams\\` was accessed inside \\`generateViewport\\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport",
"1079": "Route \"%s\": Could not validate \\`unstable_instant\\` because a Client Component in a parent segment prevented the page from rendering.",
"1080": "Route \"%s\": Runtime data such as \\`cookies()\\`, \\`headers()\\`, \\`params\\`, or \\`searchParams\\` was accessed outside of \\`<Suspense>\\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/blocking-route",
"1081": "Route \"%s\": Uncached data or \\`connection()\\` was accessed outside of \\`<Suspense>\\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/blocking-route",
"1082": "Route \"%s\": Runtime data such as \\`cookies()\\`, \\`headers()\\`, \\`params\\`, or \\`searchParams\\` was accessed inside \\`generateMetadata\\` or you have file-based metadata such as icons that depend on dynamic params segments. Except for this instance, the page would have been entirely prerenderable which may have been the intended behavior. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata",
"1083": "Route \"%s\": %s This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport"
}
15 changes: 13 additions & 2 deletions packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,10 @@ export function processMessage(
return
}
case HMR_MESSAGE_SENT_TO_BROWSER.ERRORS_TO_SHOW_IN_BROWSER: {
createFromReadableStream<Error[]>(
createFromReadableStream<{
errors: Error[]
errorCodes: Map<Error, string>
}>(
new ReadableStream({
start(controller) {
controller.enqueue(message.serializedErrors)
Expand All @@ -521,8 +524,16 @@ export function processMessage(
}),
{ findSourceMapURL }
).then(
(errors) => {
({ errors, errorCodes }) => {
for (const error of errors) {
const code = errorCodes.get(error)
if (code !== undefined) {
Object.defineProperty(error, '__NEXT_ERROR_CODE', {
value: code,
enumerable: false,
configurable: true,
})
}
console.error(error)
}
},
Expand Down
15 changes: 14 additions & 1 deletion packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ import {
isStaticGenBailoutError,
} from '../../client/components/static-generation-bailout'
import { getStackWithoutErrorMessage } from '../../lib/format-server-error'
import { extractNextErrorCode } from '../../lib/error-telemetry-utils'
import {
accessedDynamicData,
createRenderInBrowserAbortSignal,
Expand Down Expand Up @@ -3576,11 +3577,23 @@ async function logMessagesAndSendErrorsToBrowser(
)
}

// Build a Map of error → error code for errors that have one.
// React doesn't revive __NEXT_ERROR_CODE during RSC deserialization, so we
// send it as a side-channel Map. RSC preserves object identity, so the
// deserialized Map keys will reference the same Error objects.
const errorCodes = new Map<Error, string>()
for (const err of errors) {
const code = extractNextErrorCode(err)
if (code !== undefined) {
errorCodes.set(err, code)
}
}

const { clientModules } = getClientReferenceManifest()

const errorsFlightStream = renderToFlightStream(
ctx.componentMod,
errors,
{ errors, errorCodes },
clientModules,
{ filterStackFrame }
)
Expand Down
58 changes: 31 additions & 27 deletions packages/next/src/server/app-render/dynamic-rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,8 +769,8 @@ export function trackAllowedDynamicAccess(
'<Suspense>. This delays the entire page from rendering, resulting in a ' +
'slow user experience. Learn more: ' +
'https://nextjs.org/docs/messages/blocking-route'
const error = createErrorWithComponentOrOwnerStack(
message,
const error = addErrorContext(
new Error(message),
componentStack,
null
)
Expand Down Expand Up @@ -833,8 +833,8 @@ export function trackDynamicHoleInNavigation(
? `Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed inside \`generateMetadata\` or you have file-based metadata such as icons that depend on dynamic params segments.`
: `Uncached data or \`connection()\` was accessed inside \`generateMetadata\`.`
const message = `Route "${workStore.route}": ${usageDescription} Except for this instance, the page would have been entirely prerenderable which may have been the intended behavior. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata`
const error = createErrorWithComponentOrOwnerStack(
message,
const error = addErrorContext(
new Error(message),
componentStack,
dynamicValidation.createInstantStack
)
Expand All @@ -847,8 +847,8 @@ export function trackDynamicHoleInNavigation(
? `Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed inside \`generateViewport\`.`
: `Uncached data or \`connection()\` was accessed inside \`generateViewport\`.`
const message = `Route "${workStore.route}": ${usageDescription} This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport`
const error = createErrorWithComponentOrOwnerStack(
message,
const error = addErrorContext(
new Error(message),
componentStack,
dynamicValidation.createInstantStack
)
Expand Down Expand Up @@ -877,8 +877,8 @@ export function trackDynamicHoleInNavigation(
// the errors from the innermost (segments), i.e. omit layouts whose
// slots managed to render (because clearly they didn't block validation)
const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because a Client Component in a parent segment prevented the page from rendering.`
const error = createErrorWithComponentOrOwnerStack(
message,
const error = addErrorContext(
new Error(message),
componentStack,
dynamicValidation.createInstantStack
)
Expand Down Expand Up @@ -935,8 +935,8 @@ export function trackDynamicHoleInNavigation(
? `Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed outside of \`<Suspense>\`.`
: `Uncached data or \`connection()\` was accessed outside of \`<Suspense>\`.`
const message = `Route "${workStore.route}": ${usageDescription} This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/blocking-route`
const error = createErrorWithComponentOrOwnerStack(
message,
const error = addErrorContext(
new Error(message),
componentStack,
dynamicValidation.createInstantStack
)
Expand Down Expand Up @@ -968,8 +968,8 @@ export function trackDynamicHoleInRuntimeShell(
return
} else if (hasMetadataRegex.test(componentStack)) {
const message = `Route "${workStore.route}": Uncached data or \`connection()\` was accessed inside \`generateMetadata\`. Except for this instance, the page would have been entirely prerenderable which may have been the intended behavior. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata`
const error = createErrorWithComponentOrOwnerStack(
message,
const error = addErrorContext(
new Error(message),
componentStack,
null
)
Expand All @@ -980,8 +980,8 @@ export function trackDynamicHoleInRuntimeShell(
// we won't find out if there's a suspense-above-body and error for dynamic viewport
// even if there is in fact a suspense-above-body
const message = `Route "${workStore.route}": Uncached data or \`connection()\` was accessed inside \`generateViewport\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport`
const error = createErrorWithComponentOrOwnerStack(
message,
const error = addErrorContext(
new Error(message),
componentStack,
null
)
Expand Down Expand Up @@ -1012,8 +1012,8 @@ export function trackDynamicHoleInRuntimeShell(
}

const message = `Route "${workStore.route}": Uncached data or \`connection()\` was accessed outside of \`<Suspense>\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/blocking-route`
const error = createErrorWithComponentOrOwnerStack(
message,
const error = addErrorContext(
new Error(message),
componentStack,
null
)
Expand All @@ -1032,17 +1032,17 @@ export function trackDynamicHoleInStaticShell(
return
} else if (hasMetadataRegex.test(componentStack)) {
const message = `Route "${workStore.route}": Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed inside \`generateMetadata\` or you have file-based metadata such as icons that depend on dynamic params segments. Except for this instance, the page would have been entirely prerenderable which may have been the intended behavior. See more info here: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata`
const error = createErrorWithComponentOrOwnerStack(
message,
const error = addErrorContext(
new Error(message),
componentStack,
null
)
dynamicValidation.dynamicMetadata = error
return
} else if (hasViewportRegex.test(componentStack)) {
const message = `Route "${workStore.route}": Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed inside \`generateViewport\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport`
const error = createErrorWithComponentOrOwnerStack(
message,
const error = addErrorContext(
new Error(message),
componentStack,
null
)
Expand Down Expand Up @@ -1072,8 +1072,8 @@ export function trackDynamicHoleInStaticShell(
return
} else {
const message = `Route "${workStore.route}": Runtime data such as \`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` was accessed outside of \`<Suspense>\`. This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/blocking-route`
const error = createErrorWithComponentOrOwnerStack(
message,
const error = addErrorContext(
new Error(message),
componentStack,
null
)
Expand All @@ -1085,9 +1085,12 @@ export function trackDynamicHoleInStaticShell(
/**
* In dev mode, we prefer using the owner stack, otherwise the provided
* component stack is used.
*
* Accepts an already-created Error so the SWC error-code plugin can see the
* `new Error(...)` call at each call site and auto-assign error codes.
*/
function createErrorWithComponentOrOwnerStack(
message: string,
function addErrorContext(
error: Error,
componentStack: string,
createInstantStack: (() => Error) | null
) {
Expand All @@ -1096,11 +1099,12 @@ function createErrorWithComponentOrOwnerStack(
? React.captureOwnerStack()
: null

const cause = createInstantStack !== null ? createInstantStack() : null
const error = new Error(message, cause !== null ? { cause } : undefined)
if (createInstantStack !== null) {
error.cause = createInstantStack()
}
// TODO go back to owner stack here if available. This is temporarily using componentStack to get the right
//
error.stack = error.name + ': ' + message + (ownerStack || componentStack)
error.stack = error.name + ': ' + error.message + (ownerStack || componentStack)
return error
}

Expand Down
8 changes: 8 additions & 0 deletions test/lib/add-redbox-matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getRedboxComponentStack,
getRedboxDescription,
getRedboxEnvironmentLabel,
getRedboxErrorCode,
getRedboxSource,
getRedboxLabel,
getRedboxTotalErrorCount,
Expand Down Expand Up @@ -86,6 +87,7 @@ export interface ErrorSnapshot {
description?: string
componentStack?: string
cause?: SanitizedCauseEntry[]
code?: string
source: string | null
stack: string[] | null
}
Expand Down Expand Up @@ -191,6 +193,7 @@ async function createErrorSnapshot(
stack,
componentStack,
cause,
code,
] = await Promise.all([
includeLabel ? getRedboxLabel(browser) : null,
getRedboxEnvironmentLabel(browser),
Expand All @@ -199,6 +202,7 @@ async function createErrorSnapshot(
getRedboxCallStack(browser),
getRedboxComponentStack(browser),
getRedboxCause(browser),
getRedboxErrorCode(browser),
])

// We don't need to test the codeframe logic everywhere.
Expand Down Expand Up @@ -253,6 +257,10 @@ async function createErrorSnapshot(
snapshot.componentStack = componentStack
}

if (code !== null) {
snapshot.code = code
}

// Error.cause chain is only relevant when present.
if (cause !== null) {
snapshot.cause = cause.map((entry) => {
Expand Down
16 changes: 16 additions & 0 deletions test/lib/next-test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,22 @@ export function getRedboxDescription(
})
}

export function getRedboxErrorCode(
browser: Playwright
): Promise<string | null> {
return browser.eval(() => {
const portal = [].slice
.call(document.querySelectorAll('nextjs-portal'))
.find((p) => p.shadowRoot.querySelector('[data-nextjs-dialog-header]'))
const root = portal.shadowRoot
return (
root
.querySelector('[data-nextjs-error-code]')
?.getAttribute('data-nextjs-error-code') ?? null
)
})
}

export function getRedboxDescriptionWarning(
browser: Playwright
): Promise<string | null> {
Expand Down
Loading