Skip to content

Commit

Permalink
Dynamic IO: Improve error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
unstubbable committed Dec 6, 2024
1 parent fd18c85 commit f0fd675
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 80 deletions.
143 changes: 97 additions & 46 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import {
createHTMLErrorHandler,
type DigestedError,
isUserLandError,
getDigestForWellKnownError,
} from './create-error-handler'
import {
getShortDynamicParamType,
Expand Down Expand Up @@ -152,6 +153,7 @@ import { getRevalidateReason } from '../instrumentation/utils'
import { PAGE_SEGMENT_KEY } from '../../shared/lib/segment'
import type { FallbackRouteParams } from '../request/fallback-params'
import { DynamicServerError } from '../../client/components/hooks-server-context'
import { ServerPrerenderStreamResult } from './app-render-prerender-utils'
import {
type ReactServerPrerenderResult,
ReactServerResult,
Expand Down Expand Up @@ -570,7 +572,7 @@ async function generateDynamicFlightRenderResult(
ctx.clientReferenceManifest,
ctx.workStore.route,
requestStore
)
).catch(resolveValidation) // avoid unhandled rejections and a forever hanging promise
}

// For app dir, use the bundled version of Flight server renderer (renderToReadableStream)
Expand Down Expand Up @@ -1693,7 +1695,7 @@ async function renderToStream(
clientReferenceManifest,
workStore.route,
requestStore
)
).catch(resolveValidation) // avoid unhandled rejections and a forever hanging promise

reactServerResult = new ReactServerResult(reactServerStream)
} else {
Expand Down Expand Up @@ -2088,7 +2090,13 @@ async function spawnDynamicValidationInDev(
firstAttemptRSCPayload,
clientReferenceManifest.clientModules,
{
onError: (err: unknown) => {
onError: (err) => {
const digest = getDigestForWellKnownError(err)

if (digest) {
return digest
}

if (
initialServerPrerenderController.signal.aborted ||
initialServerRenderController.signal.aborted
Expand Down Expand Up @@ -2146,7 +2154,13 @@ async function spawnDynamicValidationInDev(
/>,
{
signal: initialClientController.signal,
onError: (err: unknown, _errorInfo: ErrorInfo) => {
onError: (err) => {
const digest = getDigestForWellKnownError(err)

if (digest) {
return digest
}

if (initialClientController.signal.aborted) {
// These are expected errors that might error the prerender. we ignore them.
} else if (
Expand Down Expand Up @@ -2239,12 +2253,15 @@ async function spawnDynamicValidationInDev(
finalServerPayload,
clientReferenceManifest.clientModules,
{
onError: (err: unknown) => {
if (finalServerController.signal.aborted) {
if (isPrerenderInterruptedError(err)) {
return err.digest
}
onError: (err) => {
if (
finalServerController.signal.aborted &&
isPrerenderInterruptedError(err)
) {
return err.digest
}

return getDigestForWellKnownError(err)
},
signal: finalServerController.signal,
}
Expand Down Expand Up @@ -2272,15 +2289,14 @@ async function spawnDynamicValidationInDev(
/>,
{
signal: finalClientController.signal,
onError: (err: unknown, errorInfo: ErrorInfo) => {
onError: (err, errorInfo) => {
if (
isPrerenderInterruptedError(err) ||
finalClientController.signal.aborted
) {
requestStore.usedDynamic = true

const componentStack: string | undefined = (errorInfo as any)
.componentStack
const componentStack = errorInfo.componentStack
if (typeof componentStack === 'string') {
trackAllowedDynamicAccess(
route,
Expand All @@ -2292,6 +2308,8 @@ async function spawnDynamicValidationInDev(
}
return
}

return getDigestForWellKnownError(err)
},
}
),
Expand Down Expand Up @@ -2444,7 +2462,10 @@ async function prerenderToStream(
onHTMLRenderSSRError
)

let reactServerPrerenderResult: null | ReactServerPrerenderResult = null
let reactServerPrerenderResult:
| null
| ReactServerPrerenderResult
| ServerPrerenderStreamResult = null
const setHeader = (name: string, value: string | string[]) => {
res.setHeader(name, value)

Expand Down Expand Up @@ -2525,7 +2546,13 @@ async function prerenderToStream(
initialServerPayload,
clientReferenceManifest.clientModules,
{
onError: (err: unknown) => {
onError: (err) => {
const digest = getDigestForWellKnownError(err)

if (digest) {
return digest
}

if (initialServerPrerenderController.signal.aborted) {
// The render aborted before this error was handled which indicates
// the error is caused by unfinished components within the render
Expand Down Expand Up @@ -2612,7 +2639,13 @@ async function prerenderToStream(
/>,
{
signal: initialClientController.signal,
onError: (err: unknown, _errorInfo: ErrorInfo) => {
onError: (err) => {
const digest = getDigestForWellKnownError(err)

if (digest) {
return digest
}

if (initialClientController.signal.aborted) {
// These are expected errors that might error the prerender. we ignore them.
} else if (
Expand Down Expand Up @@ -2999,7 +3032,13 @@ async function prerenderToStream(
firstAttemptRSCPayload,
clientReferenceManifest.clientModules,
{
onError: (err: unknown) => {
onError: (err) => {
const digest = getDigestForWellKnownError(err)

if (digest) {
return digest
}

if (
initialServerPrerenderController.signal.aborted ||
initialServerRenderController.signal.aborted
Expand Down Expand Up @@ -3057,7 +3096,13 @@ async function prerenderToStream(
/>,
{
signal: initialClientController.signal,
onError: (err: unknown, _errorInfo: ErrorInfo) => {
onError: (err) => {
const digest = getDigestForWellKnownError(err)

if (digest) {
return digest
}

if (initialClientController.signal.aborted) {
// These are expected errors that might error the prerender. we ignore them.
} else if (
Expand Down Expand Up @@ -3155,33 +3200,34 @@ async function prerenderToStream(
res.statusCode === 404
)

const serverPrerenderStreamResult = await prerenderServerWithPhases(
finalServerController.signal,
() =>
workUnitAsyncStorage.run(
finalServerPrerenderStore,
ComponentMod.renderToReadableStream,
finalServerPayload,
clientReferenceManifest.clientModules,
{
onError: (err: unknown) => {
if (finalServerController.signal.aborted) {
serverIsDynamic = true
if (isPrerenderInterruptedError(err)) {
return err.digest
const serverPrerenderStreamResult = (reactServerPrerenderResult =
await prerenderServerWithPhases(
finalServerController.signal,
() =>
workUnitAsyncStorage.run(
finalServerPrerenderStore,
ComponentMod.renderToReadableStream,
finalServerPayload,
clientReferenceManifest.clientModules,
{
onError: (err: unknown) => {
if (finalServerController.signal.aborted) {
serverIsDynamic = true
if (isPrerenderInterruptedError(err)) {
return err.digest
}
return getDigestForWellKnownError(err)
}
return
}

return serverComponentsErrorHandler(err)
},
signal: finalServerController.signal,
}
),
() => {
finalServerController.abort()
}
)
return serverComponentsErrorHandler(err)
},
signal: finalServerController.signal,
}
),
() => {
finalServerController.abort()
}
))

let htmlStream
const serverPhasedStream = serverPrerenderStreamResult.asPhasedStream()
Expand Down Expand Up @@ -3764,17 +3810,22 @@ async function prerenderToStream(
}

const validateRootLayout = renderOpts.dev

// This is intentionally using the readable datastream from the main
// render rather than the flight data from the error page render
const flightStream =
reactServerPrerenderResult instanceof ServerPrerenderStreamResult
? reactServerPrerenderResult.asStream()
: reactServerPrerenderResult.consumeAsStream()

return {
// Returning the error that was thrown so it can be used to handle
// the response in the caller.
digestErrorsMap: reactServerErrorsByDigest,
ssrErrors: allCapturedErrors,
stream: await continueFizzStream(fizzStream, {
inlinedDataStream: createInlinedDataReadableStream(
// This is intentionally using the readable datastream from the
// main render rather than the flight data from the error page
// render
reactServerPrerenderResult.consumeAsStream(),
flightStream,
ctx.nonce,
formState
),
Expand Down
63 changes: 33 additions & 30 deletions packages/next/src/server/app-render/create-error-handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@ type SSRErrorHandler = (

export type DigestedError = Error & { digest: string }

/**
* Returns a digest for well-known Next.js errors, otherwise `undefined`. If a
* digest is returned this also means that the error does not need to be
* reported.
*/
export function getDigestForWellKnownError(error: unknown): string | undefined {
// If we're bailing out to CSR, we don't need to log the error.
if (isBailoutToCSRError(error)) return error.digest

// If this is a navigation error, we don't need to log the error.
if (isNextRouterError(error)) return error.digest

// If this error occurs, we know that we should be stopping the static
// render. This is only thrown in static generation when PPR is not enabled,
// which causes the whole page to be marked as dynamic. We don't need to
// tell the user about this error, as it's not actionable.
if (isDynamicServerError(error)) return error.digest

return undefined
}

export function createFlightReactServerErrorHandler(
shouldFormatError: boolean,
onReactServerRenderError: (err: DigestedError) => void
Expand All @@ -34,17 +55,11 @@ export function createFlightReactServerErrorHandler(
// If the response was closed, we don't need to log the error.
if (isAbortError(thrownValue)) return

// If we're bailing out to CSR, we don't need to log the error.
if (isBailoutToCSRError(thrownValue)) return thrownValue.digest
const digest = getDigestForWellKnownError(thrownValue)

// If this is a navigation error, we don't need to log the error.
if (isNextRouterError(thrownValue)) return thrownValue.digest

// If this error occurs, we know that we should be stopping the static
// render. This is only thrown in static generation when PPR is not enabled,
// which causes the whole page to be marked as dynamic. We don't need to
// tell the user about this error, as it's not actionable.
if (isDynamicServerError(thrownValue)) return thrownValue.digest
if (digest) {
return digest
}

const err = getProperError(thrownValue) as DigestedError

Expand Down Expand Up @@ -92,17 +107,11 @@ export function createHTMLReactServerErrorHandler(
// If the response was closed, we don't need to log the error.
if (isAbortError(thrownValue)) return

// If we're bailing out to CSR, we don't need to log the error.
if (isBailoutToCSRError(thrownValue)) return thrownValue.digest
const digest = getDigestForWellKnownError(thrownValue)

// If this is a navigation error, we don't need to log the error.
if (isNextRouterError(thrownValue)) return thrownValue.digest

// If this error occurs, we know that we should be stopping the static
// render. This is only thrown in static generation when PPR is not enabled,
// which causes the whole page to be marked as dynamic. We don't need to
// tell the user about this error, as it's not actionable.
if (isDynamicServerError(thrownValue)) return thrownValue.digest
if (digest) {
return digest
}

const err = getProperError(thrownValue) as DigestedError

Expand Down Expand Up @@ -168,17 +177,11 @@ export function createHTMLErrorHandler(
// If the response was closed, we don't need to log the error.
if (isAbortError(thrownValue)) return

// If we're bailing out to CSR, we don't need to log the error.
if (isBailoutToCSRError(thrownValue)) return thrownValue.digest
const digest = getDigestForWellKnownError(thrownValue)

// If this is a navigation error, we don't need to log the error.
if (isNextRouterError(thrownValue)) return thrownValue.digest

// If this error occurs, we know that we should be stopping the static
// render. This is only thrown in static generation when PPR is not enabled,
// which causes the whole page to be marked as dynamic. We don't need to
// tell the user about this error, as it's not actionable.
if (isDynamicServerError(thrownValue)) return thrownValue.digest
if (digest) {
return digest
}

const err = getProperError(thrownValue) as DigestedError
// If the error already has a digest, respect the original digest,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { getDigestForWellKnownError } from './create-error-handler'

export function printDebugThrownValueForProspectiveRender(
thrownValue: unknown,
route: string
) {
// We don't need to print well-known Next.js errors.
if (getDigestForWellKnownError(thrownValue)) {
return
}

let message: undefined | string
if (
typeof thrownValue === 'object' &&
Expand Down
Loading

0 comments on commit f0fd675

Please sign in to comment.