From d0c39890cb62cb7d16b0f429a19c828451f489a4 Mon Sep 17 00:00:00 2001 From: Tyler Sebastian Date: Thu, 15 Jun 2023 16:10:36 -0700 Subject: [PATCH 1/5] remove error message from renderError --- packages/next/src/client/index.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/next/src/client/index.tsx b/packages/next/src/client/index.tsx index 402948ca67eb8..7c9c9de1428ab 100644 --- a/packages/next/src/client/index.tsx +++ b/packages/next/src/client/index.tsx @@ -397,12 +397,6 @@ function renderError(renderErrorProps: RenderErrorProps): Promise { }) } - // Make sure we log the error to the console, otherwise users can't track down issues. - console.error(err) - console.error( - `A client-side exception has occurred, see here for more info: https://nextjs.org/docs/messages/client-side-exception-occurred` - ) - return pageLoader .loadPage('/_error') .then(({ page: ErrorComponent, styleSheets }) => { From 9ce50dee1218447ff4e2720b53c0bfedcd02023c Mon Sep 17 00:00:00 2001 From: Tyler Sebastian Date: Fri, 16 Jun 2023 13:05:13 -0700 Subject: [PATCH 2/5] feat: skip error page re-render for ssr errors --- packages/next/src/client/index.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/next/src/client/index.tsx b/packages/next/src/client/index.tsx index 7c9c9de1428ab..6910a8ac1261a 100644 --- a/packages/next/src/client/index.tsx +++ b/packages/next/src/client/index.tsx @@ -93,6 +93,7 @@ __webpack_require__.miniCssF = addChunkSuffix(getMiniCssFilename) type RenderRouteInfo = PrivateRouteInfo & { App: AppComponent scroll?: { x: number; y: number } | null + isHydratePass?: boolean } type RenderErrorProps = Omit type RegisterFn = (input: [string, () => void]) => void @@ -397,6 +398,12 @@ function renderError(renderErrorProps: RenderErrorProps): Promise { }) } + // Make sure we log the error to the console, otherwise users can't track down issues. + console.error(err) + console.error( + `A client-side exception has occurred, see here for more info: https://nextjs.org/docs/messages/client-side-exception-occurred` + ) + return pageLoader .loadPage('/_error') .then(({ page: ErrorComponent, styleSheets }) => { @@ -755,7 +762,11 @@ function doRender(input: RenderRouteInfo): Promise { } async function render(renderingProps: RenderRouteInfo): Promise { - if (renderingProps.err) { + // if an error occurs in a server-side page (e.g. in getInitialProps), + // skip re-rendering the error page client-side as data-fetching operations + // will already have been done on the server and NEXT_DATA contains the correct + // data for straight-forward hydration of the error page + if (renderingProps.err && !renderingProps.isHydratePass) { await renderError(renderingProps) return } @@ -924,6 +935,7 @@ export async function hydrate(opts?: { beforeRender?: () => Promise }) { Component: CachedComponent, props: initialData.props, err: initialErr, + isHydratePass: true, } if (opts?.beforeRender) { From 4c3b5537064b27813ab105253c57ebda0295951e Mon Sep 17 00:00:00 2001 From: Tyler Sebastian Date: Mon, 26 Jun 2023 13:42:04 -0500 Subject: [PATCH 3/5] feat: error hydration tests --- .../error-hydration/error-hydration.test.ts | 89 +++++++++++++++++++ .../error-hydration/pages/_error.tsx | 11 +++ .../error-hydration/pages/no-error.tsx | 9 ++ .../error-hydration/pages/with-error.tsx | 7 ++ 4 files changed, 116 insertions(+) create mode 100644 test/production/error-hydration/error-hydration.test.ts create mode 100644 test/production/error-hydration/pages/_error.tsx create mode 100644 test/production/error-hydration/pages/no-error.tsx create mode 100644 test/production/error-hydration/pages/with-error.tsx diff --git a/test/production/error-hydration/error-hydration.test.ts b/test/production/error-hydration/error-hydration.test.ts new file mode 100644 index 0000000000000..884b9f6d7b7dd --- /dev/null +++ b/test/production/error-hydration/error-hydration.test.ts @@ -0,0 +1,89 @@ +import { NextInstance, createNextDescribe } from 'e2e-utils' + +async function setupErrorHydrationTests( + next: NextInstance, + targetPath: string +) { + const consoleMessages: string[] = [] + + const browser = await next.browser(targetPath, { + beforePageLoad(page) { + page.on('console', (event) => { + consoleMessages.push(event.text()) + }) + }, + }) + + return [browser, consoleMessages] as const +} + +createNextDescribe( + 'error-hydration', + { + files: __dirname, + }, + ({ next }) => { + // Recommended for tests that need a full browser + it('should log no error messages for server-side errors', async () => { + const [, consoleMessages] = await setupErrorHydrationTests( + next, + '/with-error' + ) + + const extraConsoleMessages = consoleMessages.filter( + (m) => + !/Failed to load resource/.test(m) || // Chrome output + !m.includes('Next.js page already hydrated') // test-harness output + ) + + expect(extraConsoleMessages.length).toBe(0) + }) + + it('should not invoke the error page getInitialProps client-side for server-side errors', async () => { + const [b] = await setupErrorHydrationTests(next, '/with-error') + + expect( + await b.eval( + () => + (window as any).__ERROR_PAGE_GET_INITIAL_PROPS_INVOKED_CLIENT_SIDE__ + ) + ).toBe(undefined) + }) + + it('should log an message for client-side errors, including the full, custom error', async () => { + const [browser, consoleMessages] = await setupErrorHydrationTests( + next, + '/no-error' + ) + + const link = await browser.elementByCss('a') + await link.click() + + expect( + consoleMessages.some((m) => m.includes('Error: custom error')) + ).toBe(true) + + expect( + consoleMessages.some((m) => + m.includes( + 'A client-side exception has occurred, see here for more info' + ) + ) + ).toBe(true) + }) + + it("invokes _error's getInitialProps for client-side errors", async () => { + const [browser] = await setupErrorHydrationTests(next, '/no-error') + + const link = await browser.elementByCss('a') + await link.click() + + expect( + await browser.eval( + () => + (window as any).__ERROR_PAGE_GET_INITIAL_PROPS_INVOKED_CLIENT_SIDE__ + ) + ).toBe(true) + }) + } +) diff --git a/test/production/error-hydration/pages/_error.tsx b/test/production/error-hydration/pages/_error.tsx new file mode 100644 index 0000000000000..4638807b6c50a --- /dev/null +++ b/test/production/error-hydration/pages/_error.tsx @@ -0,0 +1,11 @@ +export default function ErrorPage() { + return
Error Page Content
+} + +ErrorPage.getInitialProps = async () => { + if (typeof window !== 'undefined') { + ;(window as any).__ERROR_PAGE_GET_INITIAL_PROPS_INVOKED_CLIENT_SIDE__ = true + } + + return {} +} diff --git a/test/production/error-hydration/pages/no-error.tsx b/test/production/error-hydration/pages/no-error.tsx new file mode 100644 index 0000000000000..103573420414f --- /dev/null +++ b/test/production/error-hydration/pages/no-error.tsx @@ -0,0 +1,9 @@ +import Link from 'next/link' + +export default function Page() { + return ( +

+ click me +

+ ) +} diff --git a/test/production/error-hydration/pages/with-error.tsx b/test/production/error-hydration/pages/with-error.tsx new file mode 100644 index 0000000000000..98ca37a3d0a22 --- /dev/null +++ b/test/production/error-hydration/pages/with-error.tsx @@ -0,0 +1,7 @@ +export default function Page() { + return

hello world

+} + +Page.getInitialProps = () => { + throw new Error('custom error') +} From f34ac34406125560cc0872f68755123d7e8a9cc8 Mon Sep 17 00:00:00 2001 From: Tyler Sebastian Date: Wed, 7 Feb 2024 14:34:25 -0800 Subject: [PATCH 4/5] fix(WIP): top-level error results in no Component --- packages/next/src/client/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/next/src/client/index.tsx b/packages/next/src/client/index.tsx index 76574cc11822e..4259f7983d297 100644 --- a/packages/next/src/client/index.tsx +++ b/packages/next/src/client/index.tsx @@ -811,7 +811,12 @@ async function render(renderingProps: RenderRouteInfo): Promise { // skip re-rendering the error page client-side as data-fetching operations // will already have been done on the server and NEXT_DATA contains the correct // data for straight-forward hydration of the error page - if (renderingProps.err && !renderingProps.isHydratePass) { + if ( + renderingProps.err && + // renderingProps.Component might be undefined if there is a top/module-level error + (typeof renderingProps.Component === 'undefined' || + !renderingProps.isHydratePass) + ) { await renderError(renderingProps) return } From 4bdbb25dbfaaa975ba094656ebd7a9d0671f4fd5 Mon Sep 17 00:00:00 2001 From: Tyler Sebastian Date: Wed, 7 Feb 2024 15:54:44 -0800 Subject: [PATCH 5/5] fix: tests --- .../error-hydration/error-hydration.test.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/test/production/error-hydration/error-hydration.test.ts b/test/production/error-hydration/error-hydration.test.ts index 884b9f6d7b7dd..733b29dcee3f4 100644 --- a/test/production/error-hydration/error-hydration.test.ts +++ b/test/production/error-hydration/error-hydration.test.ts @@ -30,13 +30,19 @@ createNextDescribe( '/with-error' ) - const extraConsoleMessages = consoleMessages.filter( - (m) => - !/Failed to load resource/.test(m) || // Chrome output - !m.includes('Next.js page already hydrated') // test-harness output - ) + expect( + consoleMessages.find((message) => + message.startsWith('A client-side exception has occurred') + ) + ).toBeUndefined() - expect(extraConsoleMessages.length).toBe(0) + expect( + consoleMessages.find( + (message) => + message === + '{name: Internal Server Error., message: 500 - Internal Server Error., statusCode: 500}' + ) + ).toBeUndefined() }) it('should not invoke the error page getInitialProps client-side for server-side errors', async () => {