From f847daefcab46e83db2d9e4618426e7a8331ecfd Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 14 Oct 2024 12:22:45 -0700 Subject: [PATCH] [dynamicIO] warn in debug mode when prospective renders error (#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. --- .../next/src/server/app-render/app-render.tsx | 80 +++-- .../app-render/prospective-render-utils.ts | 43 +++ .../server/route-modules/app-route/module.ts | 10 +- ...namic-io-errors.prospective-errors.test.ts | 280 ++++++++++++++++++ .../app/error/page.tsx | 25 ++ .../app/indirection.tsx | 5 + .../prospective-render-errors/app/layout.tsx | 9 + .../app/null/page.tsx | 26 ++ .../app/object/page.tsx | 26 ++ .../app/routes/error/route.tsx | 16 + .../app/routes/object/route.tsx | 17 ++ .../app/routes/string/route.tsx | 17 ++ .../app/routes/undefined/route.tsx | 17 ++ .../app/string/page.tsx | 26 ++ .../prospective-render-errors/next.config.js | 13 + 15 files changed, 578 insertions(+), 32 deletions(-) create mode 100644 packages/next/src/server/app-render/prospective-render-utils.ts create mode 100644 test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.prospective-errors.test.ts create mode 100644 test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/error/page.tsx create mode 100644 test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/indirection.tsx create mode 100644 test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/layout.tsx create mode 100644 test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/null/page.tsx create mode 100644 test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/object/page.tsx create mode 100644 test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/error/route.tsx create mode 100644 test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/object/route.tsx create mode 100644 test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/string/route.tsx create mode 100644 test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/undefined/route.tsx create mode 100644 test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/string/page.tsx create mode 100644 test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/next.config.js diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index d771464c43e66..7ff48db26b9ed 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -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 { @@ -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 @@ -1975,8 +1979,6 @@ async function prerenderToStream( res.statusCode === 404 ) - let didError = false - let prospectiveRenderError: unknown = null ;( workUnitAsyncStorage.run( // The store to scope @@ -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 ).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 @@ -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 || @@ -2073,7 +2079,7 @@ async function prerenderToStream( finalAttemptRSCPayload, clientReferenceManifest.clientModules, { - onError, + onError: finalRenderOnError, onPostpone, signal: flightController.signal, } @@ -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 { @@ -2362,7 +2371,7 @@ async function prerenderToStream( firstAttemptRSCPayload, clientReferenceManifest.clientModules, { - onError, + onError: prospectiveRenderOnError, signal: flightController.signal, } ) as ReadableStream @@ -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) } } @@ -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) || @@ -2477,7 +2495,7 @@ async function prerenderToStream( finalAttemptRSCPayload, clientReferenceManifest.clientModules, { - onError, + onError: finalRenderOnError, signal: flightController.signal, } ) as ReadableStream diff --git a/packages/next/src/server/app-render/prospective-render-utils.ts b/packages/next/src/server/app-render/prospective-render-utils.ts new file mode 100644 index 0000000000000..6f3c567872365 --- /dev/null +++ b/packages/next/src/server/app-render/prospective-render-utils.ts @@ -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 +} diff --git a/packages/next/src/server/route-modules/app-route/module.ts b/packages/next/src/server/route-modules/app-route/module.ts index a70b7bb5f259d..7f0d4e9c003d6 100644 --- a/packages/next/src/server/route-modules/app-route/module.ts +++ b/packages/next/src/server/route-modules/app-route/module.ts @@ -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' @@ -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. @@ -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 ( @@ -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 + ) } } ) diff --git a/test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.prospective-errors.test.ts b/test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.prospective-errors.test.ts new file mode 100644 index 0000000000000..7c883c3963023 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.prospective-errors.test.ts @@ -0,0 +1,280 @@ +import { nextTestSetup } from 'e2e-utils' + +const isTurbopack = !!process.env.TURBOPACK + +describe(`Dynamic IO Prospective Render Errors - Debug Build`, () => { + const { next, isNextDev, skipped } = nextTestSetup({ + files: __dirname + '/fixtures/prospective-render-errors', + env: { NEXT_DEBUG_BUILD: 'true' }, + }) + + if (skipped) { + return + } + + if (isNextDev) { + // In next dev there really isn't a prospective render but we still assert we error on the first visit to each page + it('should error on the first visit to each page', async () => { + let res + + res = await next.fetch('/error') + expect(res.status).toBe(500) + res = await next.fetch('/error') + expect(res.status).toBe(200) + res = await next.fetch('/error') + expect(res.status).toBe(200) + + res = await next.fetch('/routes/error') + expect(res.status).toBe(500) + res = await next.fetch('/routes/error') + expect(res.status).toBe(200) + res = await next.fetch('/routes/error') + expect(res.status).toBe(200) + + res = await next.fetch('/null') + expect(res.status).toBe(500) + res = await next.fetch('/null') + expect(res.status).toBe(200) + res = await next.fetch('/null') + expect(res.status).toBe(200) + + // To make disambiguating cli output in the prod tests I switch from null to undefined + // for the routes version of this tests. really we're just asserting we get a coherent message + // when the thrown value is not a string or Error so null or undefined is not very important itself + res = await next.fetch('/routes/undefined') + expect(res.status).toBe(500) + res = await next.fetch('/routes/undefined') + expect(res.status).toBe(200) + res = await next.fetch('/routes/undefined') + expect(res.status).toBe(200) + + res = await next.fetch('/object') + expect(res.status).toBe(500) + res = await next.fetch('/object') + expect(res.status).toBe(200) + res = await next.fetch('/object') + expect(res.status).toBe(200) + + res = await next.fetch('/routes/object') + expect(res.status).toBe(500) + res = await next.fetch('/routes/object') + expect(res.status).toBe(200) + res = await next.fetch('/routes/object') + expect(res.status).toBe(200) + + res = await next.fetch('/string') + expect(res.status).toBe(500) + res = await next.fetch('/string') + expect(res.status).toBe(200) + res = await next.fetch('/string') + expect(res.status).toBe(200) + + res = await next.fetch('/routes/string') + expect(res.status).toBe(500) + res = await next.fetch('/routes/string') + expect(res.status).toBe(200) + res = await next.fetch('/routes/string') + expect(res.status).toBe(200) + }) + } else { + it('should log an error when the prospective render errors with an Error in a Page', async () => { + expect(next.cliOutput).toContain( + 'Error: Route /error errored during the prospective render.' + ) + expect(next.cliOutput).toContain('Original Error: BOOM (Error)') + if (!isTurbopack) { + // In turbopack we don't yet support disabling minification so this assertion won't work + expect(next.cliOutput).toContain('at ErrorFirstTime') + } + }) + + it('should log an error when the prospective render errors with a string in a Page', async () => { + expect(next.cliOutput).toContain( + 'Route /string errored during the prospective render.' + ) + expect(next.cliOutput).toContain('Original Message: BOOM (string)') + }) + + it('should log an error when the prospective render errors with null in a Page', async () => { + expect(next.cliOutput).toContain( + 'Route /null errored during the prospective render.' + ) + expect(next.cliOutput).toContain('\nnull\n') + }) + + it('should log an error when the prospective render errors with an object in a Page', async () => { + expect(next.cliOutput).toContain( + 'Route /object errored during the prospective render.' + ) + expect(next.cliOutput).toContain("{ boom: '(Object)' }") + }) + + it('should log an error when the prospective render errors with an Error in a route', async () => { + expect(next.cliOutput).toContain( + 'Error: Route /routes/error errored during the prospective render.' + ) + expect(next.cliOutput).toContain('Original Error: BOOM (Error route)') + if (!isTurbopack) { + // In turbopack we don't yet support disabling minification so this assertion won't work + expect(next.cliOutput).toContain('at errorFirstTime') + } + }) + + it('should log an error when the prospective render errors with a string in a route', async () => { + expect(next.cliOutput).toContain( + 'Route /routes/string errored during the prospective render.' + ) + expect(next.cliOutput).toContain('Original Message: BOOM (string route)') + }) + + it('should log an error when the prospective render errors with undefined in a route', async () => { + expect(next.cliOutput).toContain( + 'Route /routes/undefined errored during the prospective render.' + ) + expect(next.cliOutput).toContain('\nundefined\n') + }) + + it('should log an error when the prospective render errors with an object in a route', async () => { + expect(next.cliOutput).toContain( + 'Route /routes/object errored during the prospective render.' + ) + expect(next.cliOutput).toContain("{ boom: '(Object route)' }") + }) + } +}) + +describe(`Dynamic IO Prospective Render Errors - Standard Build`, () => { + const { next, isNextDev, skipped } = nextTestSetup({ + files: __dirname + '/fixtures/prospective-render-errors', + }) + + if (skipped) { + return + } + + if (isNextDev) { + // In next dev there really isn't a prospective render but we still assert we error on the first visit to each page + it('should error on the first visit to each page', async () => { + let res + + res = await next.fetch('/error') + expect(res.status).toBe(500) + res = await next.fetch('/error') + expect(res.status).toBe(200) + res = await next.fetch('/error') + expect(res.status).toBe(200) + + res = await next.fetch('/routes/error') + expect(res.status).toBe(500) + res = await next.fetch('/routes/error') + expect(res.status).toBe(200) + res = await next.fetch('/routes/error') + expect(res.status).toBe(200) + + res = await next.fetch('/null') + expect(res.status).toBe(500) + res = await next.fetch('/null') + expect(res.status).toBe(200) + res = await next.fetch('/null') + expect(res.status).toBe(200) + + // To make disambiguating cli output in the prod tests I switch from null to undefined + // for the routes version of this tests. really we're just asserting we get a coherent message + // when the thrown value is not a string or Error so null or undefined is not very important itself + res = await next.fetch('/routes/undefined') + expect(res.status).toBe(500) + res = await next.fetch('/routes/undefined') + expect(res.status).toBe(200) + res = await next.fetch('/routes/undefined') + expect(res.status).toBe(200) + + res = await next.fetch('/object') + expect(res.status).toBe(500) + res = await next.fetch('/object') + expect(res.status).toBe(200) + res = await next.fetch('/object') + expect(res.status).toBe(200) + + res = await next.fetch('/routes/object') + expect(res.status).toBe(500) + res = await next.fetch('/routes/object') + expect(res.status).toBe(200) + res = await next.fetch('/routes/object') + expect(res.status).toBe(200) + + res = await next.fetch('/string') + expect(res.status).toBe(500) + res = await next.fetch('/string') + expect(res.status).toBe(200) + res = await next.fetch('/string') + expect(res.status).toBe(200) + + res = await next.fetch('/routes/string') + expect(res.status).toBe(500) + res = await next.fetch('/routes/string') + expect(res.status).toBe(200) + res = await next.fetch('/routes/string') + expect(res.status).toBe(200) + }) + } else { + it('should not log an error when the prospective render errors with an Error in a Page', async () => { + expect(next.cliOutput).not.toContain( + 'Error: Route /error errored during the prospective render.' + ) + expect(next.cliOutput).not.toContain('Original Error: BOOM (Error)') + expect(next.cliOutput).not.toContain('at ErrorFirstTime') + }) + + it('should not log an error when the prospective render errors with a string in a Page', async () => { + expect(next.cliOutput).not.toContain( + 'Route /string errored during the prospective render.' + ) + expect(next.cliOutput).not.toContain('Original Message: BOOM (string)') + }) + + it('should not log an error when the prospective render errors with null in a Page', async () => { + expect(next.cliOutput).not.toContain( + 'Route /null errored during the prospective render.' + ) + expect(next.cliOutput).not.toContain('\nnull\n') + }) + + it('should not log an error when the prospective render errors with an object in a Page', async () => { + expect(next.cliOutput).not.toContain( + 'Route /object errored during the prospective render.' + ) + expect(next.cliOutput).not.toContain("{ boom: '(Object)' }") + }) + + it('should not log an error when the prospective render errors with an Error in a route', async () => { + expect(next.cliOutput).not.toContain( + 'Error: Route /routes/error errored during the prospective render.' + ) + expect(next.cliOutput).not.toContain('Original Error: BOOM (Error route)') + expect(next.cliOutput).not.toContain('at errorFirstTime') + }) + + it('should not log an error when the prospective render errors with a string in a route', async () => { + expect(next.cliOutput).not.toContain( + 'Route /routes/string errored during the prospective render.' + ) + expect(next.cliOutput).not.toContain( + 'Original Message: BOOM (string route)' + ) + }) + + it('should not log an error when the prospective render errors with undefined in a route', async () => { + expect(next.cliOutput).not.toContain( + 'Route /routes/undefined errored during the prospective render.' + ) + expect(next.cliOutput).not.toContain('\nundefined\n') + }) + + it('should not log an error when the prospective render errors with an object in a route', async () => { + expect(next.cliOutput).not.toContain( + 'Route /routes/object errored during the prospective render.' + ) + expect(next.cliOutput).not.toContain("{ boom: '(Object route)' }") + }) + } +}) diff --git a/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/error/page.tsx b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/error/page.tsx new file mode 100644 index 0000000000000..e58afbb6025d1 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/error/page.tsx @@ -0,0 +1,25 @@ +import { Indirection } from '../indirection' + +let didError = false + +export default async function Page() { + return ( + <> +

+ This page errors during the prospective render during build. It errors + on the first render during dev. +

+ + + + + ) +} + +async function ErrorFirstTime() { + if (!didError) { + didError = true + throw new Error('BOOM (Error)') + } + return null +} diff --git a/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/indirection.tsx b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/indirection.tsx new file mode 100644 index 0000000000000..a049ddcf739f9 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/indirection.tsx @@ -0,0 +1,5 @@ +'use client' + +export function Indirection({ children }: { children: React.ReactNode }) { + return children +} diff --git a/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/layout.tsx b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/layout.tsx new file mode 100644 index 0000000000000..745e32b8a8d23 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/layout.tsx @@ -0,0 +1,9 @@ +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + +
{children}
+ + + ) +} diff --git a/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/null/page.tsx b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/null/page.tsx new file mode 100644 index 0000000000000..b652da933dced --- /dev/null +++ b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/null/page.tsx @@ -0,0 +1,26 @@ +import { Indirection } from '../indirection' + +let didError = false + +export default async function Page() { + return ( + <> +

+ This page errors during the prospective render during build. It errors + on the first render during dev. +

+ + + + + ) +} + +async function ErrorFirstTime() { + if (!didError) { + didError = true + // eslint-disable-next-line no-throw-literal + throw null + } + return null +} diff --git a/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/object/page.tsx b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/object/page.tsx new file mode 100644 index 0000000000000..923694e361541 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/object/page.tsx @@ -0,0 +1,26 @@ +import { Indirection } from '../indirection' + +let didError = false + +export default async function Page() { + return ( + <> +

+ This page errors during the prospective render during build. It errors + on the first render during dev. +

+ + + + + ) +} + +async function ErrorFirstTime() { + if (!didError) { + didError = true + // eslint-disable-next-line no-throw-literal + throw { boom: '(Object)' } + } + return null +} diff --git a/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/error/route.tsx b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/error/route.tsx new file mode 100644 index 0000000000000..0fe3eb27eff52 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/error/route.tsx @@ -0,0 +1,16 @@ +let didError = false + +export async function GET() { + errorFirstTime() + return new Response( + 'This page errors during the prospective render during build. It errors on the first render during dev.' + ) +} + +function errorFirstTime() { + if (!didError) { + didError = true + throw new Error('BOOM (Error route)') + } + return null +} diff --git a/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/object/route.tsx b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/object/route.tsx new file mode 100644 index 0000000000000..fb555248ca7bc --- /dev/null +++ b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/object/route.tsx @@ -0,0 +1,17 @@ +let didError = false + +export async function GET() { + errorFirstTime() + return new Response( + 'This page errors during the prospective render during build. It errors on the first render during dev.' + ) +} + +function errorFirstTime() { + if (!didError) { + didError = true + // eslint-disable-next-line no-throw-literal + throw { boom: '(Object route)' } + } + return null +} diff --git a/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/string/route.tsx b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/string/route.tsx new file mode 100644 index 0000000000000..fc80435641f00 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/string/route.tsx @@ -0,0 +1,17 @@ +let didError = false + +export async function GET() { + errorFirstTime() + return new Response( + 'This page errors during the prospective render during build. It errors on the first render during dev.' + ) +} + +function errorFirstTime() { + if (!didError) { + didError = true + // eslint-disable-next-line no-throw-literal + throw 'BOOM (string route)' + } + return null +} diff --git a/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/undefined/route.tsx b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/undefined/route.tsx new file mode 100644 index 0000000000000..ff7f90780cd33 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/routes/undefined/route.tsx @@ -0,0 +1,17 @@ +let didError = false + +export async function GET() { + errorFirstTime() + return new Response( + 'This page errors during the prospective render during build. It errors on the first render during dev.' + ) +} + +function errorFirstTime() { + if (!didError) { + didError = true + // eslint-disable-next-line no-throw-literal + throw undefined + } + return null +} diff --git a/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/string/page.tsx b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/string/page.tsx new file mode 100644 index 0000000000000..22ea22e3b1cbf --- /dev/null +++ b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/app/string/page.tsx @@ -0,0 +1,26 @@ +import { Indirection } from '../indirection' + +let didError = false + +export default async function Page() { + return ( + <> +

+ This page errors during the prospective render during build. It errors + on the first render during dev. +

+ + + + + ) +} + +async function ErrorFirstTime() { + if (!didError) { + didError = true + // eslint-disable-next-line no-throw-literal + throw 'BOOM (string)' + } + return null +} diff --git a/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/next.config.js b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/next.config.js new file mode 100644 index 0000000000000..f91d543c0e266 --- /dev/null +++ b/test/e2e/app-dir/dynamic-io-errors/fixtures/prospective-render-errors/next.config.js @@ -0,0 +1,13 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + ppr: process.env.__NEXT_EXPERIMENTAL_PPR === 'true', + pprFallbacks: process.env.__NEXT_EXPERIMENTAL_PPR === 'true', + dynamicIO: true, + serverMinification: false, + }, +} + +module.exports = nextConfig