diff --git a/packages/next/src/client/on-recoverable-error.ts b/packages/next/src/client/on-recoverable-error.ts index f00a19073bca5..c22e5eec898e5 100644 --- a/packages/next/src/client/on-recoverable-error.ts +++ b/packages/next/src/client/on-recoverable-error.ts @@ -1,4 +1,4 @@ -import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/lazy-dynamic/no-ssr-error' +import { isBailoutCSRError } from '../shared/lib/lazy-dynamic/no-ssr-error' export default function onRecoverableError(err: any) { // Using default react onRecoverableError @@ -13,7 +13,7 @@ export default function onRecoverableError(err: any) { } // Skip certain custom errors which are not expected to be reported on client - if (err.digest === NEXT_DYNAMIC_NO_SSR_CODE) return + if (isBailoutCSRError(err)) return defaultOnRecoverableError(err) } diff --git a/packages/next/src/export/helpers/is-dynamic-usage-error.ts b/packages/next/src/export/helpers/is-dynamic-usage-error.ts index b8d324ebab110..deede653d1ae7 100644 --- a/packages/next/src/export/helpers/is-dynamic-usage-error.ts +++ b/packages/next/src/export/helpers/is-dynamic-usage-error.ts @@ -1,10 +1,10 @@ import { DYNAMIC_ERROR_CODE } from '../../client/components/hooks-server-context' import { isNotFoundError } from '../../client/components/not-found' import { isRedirectError } from '../../client/components/redirect' -import { NEXT_DYNAMIC_NO_SSR_CODE } from '../../shared/lib/lazy-dynamic/no-ssr-error' +import { isBailoutCSRError } from '../../shared/lib/lazy-dynamic/no-ssr-error' export const isDynamicUsageError = (err: any) => err.digest === DYNAMIC_ERROR_CODE || isNotFoundError(err) || - err.digest === NEXT_DYNAMIC_NO_SSR_CODE || + isBailoutCSRError(err) || isRedirectError(err) diff --git a/packages/next/src/export/routes/pages.ts b/packages/next/src/export/routes/pages.ts index 0cedd3594696f..936b5159c9e96 100644 --- a/packages/next/src/export/routes/pages.ts +++ b/packages/next/src/export/routes/pages.ts @@ -15,7 +15,7 @@ import { NEXT_DATA_SUFFIX, SERVER_PROPS_EXPORT_ERROR, } from '../../lib/constants' -import { NEXT_DYNAMIC_NO_SSR_CODE } from '../../shared/lib/lazy-dynamic/no-ssr-error' +import { isBailoutCSRError } from '../../shared/lib/lazy-dynamic/no-ssr-error' import AmpHtmlValidator from 'next/dist/compiled/amphtml-validator' import { FileType, fileExists } from '../../lib/file-exists' import { lazyRenderPagesPage } from '../../server/future/route-modules/pages/module.render' @@ -106,7 +106,7 @@ export async function exportPages( renderOpts ) } catch (err: any) { - if (err.digest !== NEXT_DYNAMIC_NO_SSR_CODE) { + if (!isBailoutCSRError(err)) { throw err } } @@ -164,7 +164,7 @@ export async function exportPages( renderOpts ) } catch (err: any) { - if (err.digest !== NEXT_DYNAMIC_NO_SSR_CODE) { + if (!isBailoutCSRError(err)) { throw err } } diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index a22b23fee4765..62c0506ae322e 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -66,7 +66,7 @@ import { parseAndValidateFlightRouterState } from './parse-and-validate-flight-r import { validateURL } from './validate-url' import { createFlightRouterStateFromLoaderTree } from './create-flight-router-state-from-loader-tree' import { handleAction } from './action-handler' -import { NEXT_DYNAMIC_NO_SSR_CODE } from '../../shared/lib/lazy-dynamic/no-ssr-error' +import { isBailoutCSRError } from '../../shared/lib/lazy-dynamic/no-ssr-error' import { warn, error } from '../../build/output/log' import { appendMutableCookies } from '../web/spec-extension/adapters/request-cookies' import { createServerInsertedHTML } from './server-inserted-html' @@ -863,7 +863,8 @@ async function renderToHTMLOrFlightImpl( throw err } - if (err.digest === NEXT_DYNAMIC_NO_SSR_CODE) { + const isBailoutCSR = isBailoutCSRError(err) + if (isBailoutCSR) { warn( `Entire page ${pagePath} deopted into client-side rendering. https://nextjs.org/docs/messages/deopted-into-client-rendering`, pagePath @@ -894,7 +895,7 @@ async function renderToHTMLOrFlightImpl( } const is404 = res.statusCode === 404 - if (!is404 && !hasRedirectError) { + if (!is404 && !hasRedirectError && !isBailoutCSR) { res.statusCode = 500 } diff --git a/packages/next/src/shared/lib/lazy-dynamic/no-ssr-error.ts b/packages/next/src/shared/lib/lazy-dynamic/no-ssr-error.ts index 8a71ae501e513..3bce562a5bb36 100644 --- a/packages/next/src/shared/lib/lazy-dynamic/no-ssr-error.ts +++ b/packages/next/src/shared/lib/lazy-dynamic/no-ssr-error.ts @@ -7,3 +7,7 @@ export function throwWithNoSSR() { ;(error as any).digest = NEXT_DYNAMIC_NO_SSR_CODE throw error } + +export function isBailoutCSRError(err: any) { + return err?.digest === NEXT_DYNAMIC_NO_SSR_CODE +} diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index d5259853cf5a3..0da68a6512f87 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -542,15 +542,12 @@ createNextDescribe( 'blog/styfle/second-post.rsc', 'force-static/[slug]/page.js', 'hooks/use-pathname/slug.rsc', - 'hooks/use-search-params.rsc', 'route-handler/post/route.js', 'blog/[author]/[slug]/page.js', 'blog/styfle/second-post.html', 'hooks/use-pathname/slug.html', - 'hooks/use-search-params.html', 'flight/[slug]/[slug2]/page.js', 'variable-revalidate/cookie.rsc', - 'hooks/use-search-params/page.js', 'ssr-auto/cache-no-store/page.js', 'variable-revalidate/cookie.html', 'api/revalidate-tag-edge/route.js', @@ -663,7 +660,10 @@ createNextDescribe( 'force-static/[slug]/page_client-reference-manifest.js', 'blog/[author]/[slug]/page_client-reference-manifest.js', 'flight/[slug]/[slug2]/page_client-reference-manifest.js', - 'hooks/use-search-params/page_client-reference-manifest.js', + 'hooks/use-search-params/static-bailout.html', + 'hooks/use-search-params/static-bailout.rsc', + 'hooks/use-search-params/static-bailout/page.js', + 'hooks/use-search-params/static-bailout/page_client-reference-manifest.js', 'ssr-auto/cache-no-store/page_client-reference-manifest.js', 'gen-params-dynamic/[slug]/page_client-reference-manifest.js', 'hooks/use-pathname/[slug]/page_client-reference-manifest.js', @@ -966,8 +966,8 @@ createNextDescribe( "initialRevalidateSeconds": false, "srcRoute": "/hooks/use-pathname/[slug]", }, - "/hooks/use-search-params": { - "dataRoute": "/hooks/use-search-params.rsc", + "/hooks/use-search-params/force-static": { + "dataRoute": "/hooks/use-search-params/force-static.rsc", "experimentalBypassFor": [ { "key": "Next-Action", @@ -980,10 +980,10 @@ createNextDescribe( }, ], "initialRevalidateSeconds": false, - "srcRoute": "/hooks/use-search-params", + "srcRoute": "/hooks/use-search-params/force-static", }, - "/hooks/use-search-params/force-static": { - "dataRoute": "/hooks/use-search-params/force-static.rsc", + "/hooks/use-search-params/static-bailout": { + "dataRoute": "/hooks/use-search-params/static-bailout.rsc", "experimentalBypassFor": [ { "key": "Next-Action", @@ -996,7 +996,7 @@ createNextDescribe( }, ], "initialRevalidateSeconds": false, - "srcRoute": "/hooks/use-search-params/force-static", + "srcRoute": "/hooks/use-search-params/static-bailout", }, "/hooks/use-search-params/with-suspense": { "dataRoute": "/hooks/use-search-params/with-suspense.rsc", @@ -2838,9 +2838,9 @@ createNextDescribe( describe('useSearchParams', () => { describe('client', () => { it('should bailout to client rendering - without suspense boundary', async () => { - const browser = await next.browser( - '/hooks/use-search-params?first=value&second=other&third' - ) + const url = + '/hooks/use-search-params/static-bailout?first=value&second=other&third' + const browser = await next.browser(url) expect(await browser.elementByCss('#params-first').text()).toBe( 'value' @@ -2852,12 +2852,15 @@ createNextDescribe( expect(await browser.elementByCss('#params-not-real').text()).toBe( 'N/A' ) + + const $ = await next.render$(url) + expect($('meta[content=noindex]').length).toBe(0) }) it('should bailout to client rendering - with suspense boundary', async () => { - const browser = await next.browser( + const url = '/hooks/use-search-params/with-suspense?first=value&second=other&third' - ) + const browser = await next.browser(url) expect(await browser.elementByCss('#params-first').text()).toBe( 'value' @@ -2869,6 +2872,11 @@ createNextDescribe( expect(await browser.elementByCss('#params-not-real').text()).toBe( 'N/A' ) + + const $ = await next.render$(url) + // dynamic page doesn't have bail out + expect($('html#__next_error__').length).toBe(0) + expect($('meta[content=noindex]').length).toBe(0) }) it.skip('should have empty search params on force-static', async () => { @@ -2919,7 +2927,9 @@ createNextDescribe( if (!isDev) { describe('server response', () => { it('should bailout to client rendering - without suspense boundary', async () => { - const res = await next.fetch('/hooks/use-search-params') + const res = await next.fetch( + '/hooks/use-search-params/static-bailout' + ) const html = await res.text() expect(html).toInclude('') }) diff --git a/test/e2e/app-dir/app-static/app/hooks/use-search-params/page.js b/test/e2e/app-dir/app-static/app/hooks/use-search-params/static-bailout/page.js similarity index 73% rename from test/e2e/app-dir/app-static/app/hooks/use-search-params/page.js rename to test/e2e/app-dir/app-static/app/hooks/use-search-params/static-bailout/page.js index 0a9904e2a08c1..bdf2409132597 100644 --- a/test/e2e/app-dir/app-static/app/hooks/use-search-params/page.js +++ b/test/e2e/app-dir/app-static/app/hooks/use-search-params/static-bailout/page.js @@ -1,4 +1,4 @@ -import UseSearchParams from './search-params' +import UseSearchParams from '../search-params' export default function Page() { return ( diff --git a/test/e2e/app-dir/app-static/next.config.js b/test/e2e/app-dir/app-static/next.config.js index c4cd47bd2aa6a..bc0912e06b554 100644 --- a/test/e2e/app-dir/app-static/next.config.js +++ b/test/e2e/app-dir/app-static/next.config.js @@ -13,7 +13,7 @@ module.exports = { afterFiles: [ { source: '/rewritten-use-search-params', - destination: '/hooks/use-search-params', + destination: '/hooks/use-search-params/static-bailout', }, { source: '/rewritten-use-pathname', diff --git a/test/e2e/app-dir/hooks/app/hooks/use-search-params/server/page.js b/test/e2e/app-dir/hooks/app/hooks/use-search-params/server/page.js deleted file mode 100644 index b84536b7ec4e9..0000000000000 --- a/test/e2e/app-dir/hooks/app/hooks/use-search-params/server/page.js +++ /dev/null @@ -1,8 +0,0 @@ -// import { useSearchParams } from 'next/navigation' - -export default function Page() { - // This should throw an error. - // useSearchParams() - - return null -} diff --git a/test/e2e/app-dir/hooks/app/layout.js b/test/e2e/app-dir/hooks/app/layout.js index 79d25f7e6fff9..ee7ec55f76e10 100644 --- a/test/e2e/app-dir/hooks/app/layout.js +++ b/test/e2e/app-dir/hooks/app/layout.js @@ -1,6 +1,6 @@ import React from 'react' -export const revalidate = 0 +export const dynamic = 'force-dynamic' export default function Layout({ children }) { return ( diff --git a/test/e2e/app-dir/hooks/hooks.test.ts b/test/e2e/app-dir/hooks/hooks.test.ts index 9538147671532..3b385e3237127 100644 --- a/test/e2e/app-dir/hooks/hooks.test.ts +++ b/test/e2e/app-dir/hooks/hooks.test.ts @@ -85,7 +85,7 @@ createNextDescribe( initialRand = $('#rand').text() }) - it('should genenerate rand when draft mode enabled', async () => { + it('should generate rand when draft mode enabled', async () => { const res = await next.fetch('/enable') const h = res.headers.get('set-cookie') || '' const cookie = h