Skip to content

Commit

Permalink
[dynamicIO] warn in debug mode when prospective renders error (vercel…
Browse files Browse the repository at this point in the history
…#71266)

When prerendering pages and routes errors with dynamicIO enabled, errors
during the prospective render may be supressed from logging. Normally
this is expected and ok because the dynamic portion of a render may be
expected to error during prerendering. However some codepaths are not
hit the same between the prospective and final renders and so it can be
useful to have the ability to get additional insight into the runtime
behavior of the prospective render.

This change adds extra logging when `process.env.NEXT_DEBUG_BUILD` is
enabled. When this environment variable is set we log errors that throw
during the prospective render.
  • Loading branch information
gnoff authored Oct 14, 2024
1 parent fc2a7d2 commit f847dae
Show file tree
Hide file tree
Showing 15 changed files with 578 additions and 32 deletions.
80 changes: 49 additions & 31 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ import {
createReactServerPrerenderResultFromRender,
prerenderAndAbortInSequentialTasks,
} from './app-render-prerender-utils'
import { printDebugThrownValueForProspectiveRender } from './prospective-render-utils'
import { scheduleInSequentialTasks } from './app-render-render-utils'
import { waitAtLeastOneReactRenderTask } from '../../lib/scheduler'
import {
Expand Down Expand Up @@ -1953,16 +1954,19 @@ async function prerenderToStream(
})

let reactServerIsDynamic = false
function onError(err: unknown) {
function prospectiveRenderOnError(err: unknown) {
if (err === abortReason) {
reactServerIsDynamic = true
return PRERENDER_COMPLETE
} else if (isPrerenderInterruptedError(err)) {
reactServerIsDynamic = true
return err.digest
} else if (process.env.NEXT_DEBUG_BUILD) {
printDebugThrownValueForProspectiveRender(err, workStore.route)
}

return serverComponentsErrorHandler(err)
// We don't track errors during the prospective render because we will
// always do a final render and we cannot infer the errors from this render
// are relevant to the final result
}

// We're not going to use the result of this render because the only time it could be used
Expand All @@ -1975,8 +1979,6 @@ async function prerenderToStream(
res.statusCode === 404
)

let didError = false
let prospectiveRenderError: unknown = null
;(
workUnitAsyncStorage.run(
// The store to scope
Expand All @@ -1987,35 +1989,27 @@ async function prerenderToStream(
firstAttemptRSCPayload,
clientReferenceManifest.clientModules,
{
// This render will be thrown away so we don't need to track errors or postpones
onError,
onPostpone: undefined,
onError: prospectiveRenderOnError,
// we don't care to track postpones during the prospective render because we need
// to always do a final render anyway
onPostpone: undefined,
signal: flightController.signal,
}
) as Promise<ReactServerPrerenderResolveToType>
).catch((err) => {
if (
process.env.NEXT_DEBUG_BUILD &&
err !== abortReason &&
!isPrerenderInterruptedError(err) &&
!isDynamicServerError(err)
) {
didError = true
prospectiveRenderError = err
printDebugThrownValueForProspectiveRender(err, workStore.route)
}
})

// When this resolves the cache has no inflight reads and we can ascertain the dynamic outcome
await cacheSignal.cacheReady()
flightController.abort(abortReason)
// We wait a microtask to to ensure the catch handler has a chance to run if the root errors when we abort.
await 1
if (didError) {
// We errored with something other than prerender errors during the warmup. We throw here
// to allow the user error to be handled
throw prospectiveRenderError
}

// When PPR is enabled we don't synchronously abort the render when performing a prospective render
// because it might prevent us from discovering all caches during the render which is essential
Expand Down Expand Up @@ -2045,6 +2039,18 @@ async function prerenderToStream(
tags: [...ctx.requestStore.implicitTags],
})

function finalRenderOnError(err: unknown) {
if (err === abortReason) {
reactServerIsDynamic = true
return PRERENDER_COMPLETE
} else if (isPrerenderInterruptedError(err)) {
reactServerIsDynamic = true
return err.digest
}

return serverComponentsErrorHandler(err)
}

function onPostpone(reason: string) {
if (
reason === PRERENDER_COMPLETE ||
Expand Down Expand Up @@ -2073,7 +2079,7 @@ async function prerenderToStream(
finalAttemptRSCPayload,
clientReferenceManifest.clientModules,
{
onError,
onError: finalRenderOnError,
onPostpone,
signal: flightController.signal,
}
Expand Down Expand Up @@ -2340,16 +2346,19 @@ async function prerenderToStream(
let reactServerIsDynamic = false
let reactServerIsSynchronouslyDynamic = false

function onError(err: unknown) {
function prospectiveRenderOnError(err: unknown) {
if (err === abortReason) {
reactServerIsDynamic = true
return PRERENDER_COMPLETE
} else if (isPrerenderInterruptedError(err)) {
reactServerIsSynchronouslyDynamic = true
return err.digest
} else if (process.env.NEXT_DEBUG_BUILD) {
printDebugThrownValueForProspectiveRender(err, workStore.route)
}

return serverComponentsErrorHandler(err)
// We don't track errors during the prospective render because we will
// always do a final render and we cannot infer the errors from this render
// are relevant to the final result
}

try {
Expand All @@ -2362,7 +2371,7 @@ async function prerenderToStream(
firstAttemptRSCPayload,
clientReferenceManifest.clientModules,
{
onError,
onError: prospectiveRenderOnError,
signal: flightController.signal,
}
) as ReadableStream<Uint8Array>
Expand All @@ -2376,15 +2385,12 @@ async function prerenderToStream(
await warmFlightResponse(prospectiveStream, clientReferenceManifest)
} catch (err) {
if (
err === abortReason ||
isPrerenderInterruptedError(err) ||
isDynamicServerError(err)
process.env.NEXT_DEBUG_BUILD &&
err !== abortReason &&
!isPrerenderInterruptedError(err) &&
!isDynamicServerError(err)
) {
// We aborted with an incomplete shell. We'll handle this below with the handling
// for dynamic.
} else {
// We have some other kind of shell error, we want to bubble this up to be handled
throw err
printDebugThrownValueForProspectiveRender(err, workStore.route)
}
}

Expand Down Expand Up @@ -2440,6 +2446,18 @@ async function prerenderToStream(
res.statusCode === 404
)

function finalRenderOnError(err: unknown) {
if (err === abortReason) {
reactServerIsDynamic = true
return PRERENDER_COMPLETE
} else if (isPrerenderInterruptedError(err)) {
reactServerIsDynamic = true
return err.digest
}

return serverComponentsErrorHandler(err)
}

function SSROnError(err: unknown, errorInfo?: ErrorInfo) {
if (
isAbortReason(err, abortReason) ||
Expand Down Expand Up @@ -2477,7 +2495,7 @@ async function prerenderToStream(
finalAttemptRSCPayload,
clientReferenceManifest.clientModules,
{
onError,
onError: finalRenderOnError,
signal: flightController.signal,
}
) as ReadableStream<Uint8Array>
Expand Down
43 changes: 43 additions & 0 deletions packages/next/src/server/app-render/prospective-render-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export function printDebugThrownValueForProspectiveRender(
thrownValue: unknown,
route: string
) {
let message: undefined | string
if (
typeof thrownValue === 'object' &&
thrownValue !== null &&
typeof (thrownValue as any).message === 'string'
) {
message = (thrownValue as any).message
if (typeof (thrownValue as any).stack === 'string') {
const originalErrorStack: string = (thrownValue as any).stack
const stackStart = originalErrorStack.indexOf('\n')
if (stackStart > -1) {
const error = new Error(
`Route ${route} errored during the prospective render. These errors are normally ignored and may not prevent the route from prerendering but are logged here because build debugging is enabled.
Original Error: ${message}`
)
error.stack =
'Error: ' + error.message + originalErrorStack.slice(stackStart)
console.error(error)
return
}
}
} else if (typeof thrownValue === 'string') {
message = thrownValue
}

if (message) {
console.error(`Route ${route} errored during the prospective render. These errors are normally ignored and may not prevent the route from prerendering but are logged here because build debugging is enabled. No stack was provided.
Original Message: ${message}`)
return
}

console.error(
`Route ${route} errored during the prospective render. These errors are normally ignored and may not prevent the route from prerendering but are logged here because build debugging is enabled. The thrown value is logged just following this message`
)
console.error(thrownValue)
return
}
10 changes: 9 additions & 1 deletion packages/next/src/server/route-modules/app-route/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
import { HeadersAdapter } from '../../web/spec-extension/adapters/headers'
import { RequestCookiesAdapter } from '../../web/spec-extension/adapters/request-cookies'
import { parsedUrlQueryToParams } from './helpers/parsed-url-query-to-params'
import { printDebugThrownValueForProspectiveRender } from '../../app-render/prospective-render-utils'

import * as serverHooks from '../../../client/components/hooks-server-context'
import { DynamicServerError } from '../../../client/components/hooks-server-context'
Expand Down Expand Up @@ -327,7 +328,7 @@ export class AppRouteRouteModule extends RouteModule<
*
* Next we run the handler again and we check if we get a result back in a microtask.
* Next.js expects the return value to be a Response or a Thenable that resolves to a Response.
* Unfortunately Response's do not allow for acessing the response body synchronously or in
* Unfortunately Response's do not allow for accessing the response body synchronously or in
* a microtask so we need to allow one more task to unwrap the response body. This is a slightly
* different semantic than what we have when we render and it means that certain tasks can still
* execute before a prerender completes such as a carefully timed setImmediate.
Expand Down Expand Up @@ -370,6 +371,8 @@ export class AppRouteRouteModule extends RouteModule<
// the route handler called an API which is always dynamic
// there is no need to try again
prospectiveRenderIsDynamic = true
} else if (process.env.NEXT_DEBUG_BUILD) {
printDebugThrownValueForProspectiveRender(err, workStore.route)
}
}
if (
Expand All @@ -386,6 +389,11 @@ export class AppRouteRouteModule extends RouteModule<
// the route handler called an API which is always dynamic
// there is no need to try again
prospectiveRenderIsDynamic = true
} else if (process.env.NEXT_DEBUG_BUILD) {
printDebugThrownValueForProspectiveRender(
err,
workStore.route
)
}
}
)
Expand Down
Loading

0 comments on commit f847dae

Please sign in to comment.