From d7335b75d14bfecf65d6905dd64e25181b8c255a Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 20 Jul 2023 20:39:05 -0700 Subject: [PATCH] Revert "Refine the not-found rendering process for app router" (#52977) Reverts vercel/next.js#52790 Reverting temporarily as this breaks turbopack's not found handling due to the app tree being generated there not having the necessary parallel routes in the `_not-found` entry x-ref: https://github.com/vercel/next.js/blob/0df8aac935741808ee6aee78a8b545b2c1405f29/packages/next-swc/crates/next-core/src/app_structure.rs#L677-L681 x-ref: https://github.com/vercel/next.js/actions/runs/5616458194/job/15220295829 --- .../next/src/client/components/app-router.tsx | 41 ++- .../dev-root-not-found-boundary.tsx | 25 -- .../src/client/components/layout-router.tsx | 3 + .../client/components/not-found-boundary.tsx | 3 - .../react-dev-overlay/hot-reloader-client.tsx | 62 ++-- .../internal/ReactDevOverlay.tsx | 5 + .../internal/error-overlay-reducer.ts | 12 +- .../next/src/server/app-render/app-render.tsx | 346 +++++++----------- .../create-server-components-renderer.tsx | 15 + .../next/src/server/lib/app-dir-module.ts | 2 +- packages/next/src/server/render.tsx | 1 + .../stream-utils/node-web-streams-helper.ts | 19 - .../app-dir/actions/app/server/client-form.js | 2 +- test/e2e/app-dir/actions/app/server/form.js | 2 +- test/e2e/app-dir/metadata/app/not-found.tsx | 2 +- .../e2e/app-dir/navigation/navigation.test.ts | 7 - test/e2e/app-dir/not-found/app/layout.js | 14 +- test/e2e/app-dir/not-found/app/not-found.js | 4 +- test/e2e/app-dir/not-found/not-found.test.ts | 8 +- .../root-layout-not-found/app/layout.js | 27 -- .../app/not-found-trigger.js | 12 - .../app-dir/root-layout-not-found/app/page.js | 3 - .../root-layout-not-found/index.test.ts | 52 --- 23 files changed, 246 insertions(+), 421 deletions(-) delete mode 100644 packages/next/src/client/components/dev-root-not-found-boundary.tsx delete mode 100644 test/e2e/app-dir/root-layout-not-found/app/layout.js delete mode 100644 test/e2e/app-dir/root-layout-not-found/app/not-found-trigger.js delete mode 100644 test/e2e/app-dir/root-layout-not-found/app/page.js delete mode 100644 test/e2e/app-dir/root-layout-not-found/index.test.ts diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx index b00f6ff57c34c..7aefd5773f859 100644 --- a/packages/next/src/client/components/app-router.tsx +++ b/packages/next/src/client/components/app-router.tsx @@ -54,6 +54,7 @@ import { isBot } from '../../shared/lib/router/utils/is-bot' import { addBasePath } from '../add-base-path' import { AppRouterAnnouncer } from './app-router-announcer' import { RedirectBoundary } from './redirect-boundary' +import { NotFoundBoundary } from './not-found-boundary' import { findHeadInCache } from './router-reducer/reducers/find-head-in-cache' import { createInfinitePromise } from './infinite-promise' import { NEXT_RSC_UNION_QUERY } from './app-router-headers' @@ -88,6 +89,14 @@ export function urlToUrlWithoutFlightMarker(url: string): URL { return urlWithoutFlightParameters } +const HotReloader: + | typeof import('./react-dev-overlay/hot-reloader-client').default + | null = + process.env.NODE_ENV === 'production' + ? null + : (require('./react-dev-overlay/hot-reloader-client') + .default as typeof import('./react-dev-overlay/hot-reloader-client').default) + type AppRouterProps = Omit< Omit, 'initialParallelRoutes' @@ -95,6 +104,9 @@ type AppRouterProps = Omit< buildId: string initialHead: ReactNode assetPrefix: string + // Top level boundaries props + notFound: React.ReactNode | undefined + asNotFound?: boolean } function isExternalURL(url: URL) { @@ -212,6 +224,8 @@ function Router({ initialCanonicalUrl, children, assetPrefix, + notFound, + asNotFound, }: AppRouterProps) { const initialState = useMemo( () => @@ -431,7 +445,9 @@ function Router({ return findHeadInCache(cache, tree[1]) }, [cache, tree]) - let content = ( + const notFoundProps = { notFound, asNotFound } + + const content = ( {head} {cache.subTreeData} @@ -439,18 +455,6 @@ function Router({ ) - if (process.env.NODE_ENV !== 'production') { - if (typeof window !== 'undefined') { - const DevRootNotFoundBoundary: typeof import('./dev-root-not-found-boundary').DevRootNotFoundBoundary = - require('./dev-root-not-found-boundary').DevRootNotFoundBoundary - content = {content} - } - const HotReloader: typeof import('./react-dev-overlay/hot-reloader-client').default = - require('./react-dev-overlay/hot-reloader-client').default - - content = {content} - } - return ( <> - {content} + {HotReloader ? ( + // HotReloader implements a separate NotFoundBoundary to maintain the HMR ping interval + + {content} + + ) : ( + + {content} + + )} diff --git a/packages/next/src/client/components/dev-root-not-found-boundary.tsx b/packages/next/src/client/components/dev-root-not-found-boundary.tsx deleted file mode 100644 index d2391062104c3..0000000000000 --- a/packages/next/src/client/components/dev-root-not-found-boundary.tsx +++ /dev/null @@ -1,25 +0,0 @@ -'use client' - -import React from 'react' -import { NotFoundBoundary } from './not-found-boundary' - -export function bailOnNotFound() { - throw new Error('notFound() is not allowed to use in root layout') -} - -function NotAllowedRootNotFoundError() { - bailOnNotFound() - return null -} - -export function DevRootNotFoundBoundary({ - children, -}: { - children: React.ReactNode -}) { - return ( - }> - {children} - - ) -} diff --git a/packages/next/src/client/components/layout-router.tsx b/packages/next/src/client/components/layout-router.tsx index 2cd8ce9309ca2..d3c9a0733383d 100644 --- a/packages/next/src/client/components/layout-router.tsx +++ b/packages/next/src/client/components/layout-router.tsx @@ -491,6 +491,7 @@ export default function OuterLayoutRouter({ template, notFound, notFoundStyles, + asNotFound, styles, }: { parallelRouterKey: string @@ -505,6 +506,7 @@ export default function OuterLayoutRouter({ hasLoading: boolean notFound: React.ReactNode | undefined notFoundStyles: React.ReactNode | undefined + asNotFound?: boolean styles?: React.ReactNode }) { const context = useContext(LayoutRouterContext) @@ -572,6 +574,7 @@ export default function OuterLayoutRouter({ - {process.env.NODE_ENV === 'development' && ( - - )} {this.props.notFoundStyles} {this.props.notFound} diff --git a/packages/next/src/client/components/react-dev-overlay/hot-reloader-client.tsx b/packages/next/src/client/components/react-dev-overlay/hot-reloader-client.tsx index e049d3f22a397..878abd5fd9f71 100644 --- a/packages/next/src/client/components/react-dev-overlay/hot-reloader-client.tsx +++ b/packages/next/src/client/components/react-dev-overlay/hot-reloader-client.tsx @@ -10,6 +10,7 @@ import stripAnsi from 'next/dist/compiled/strip-ansi' import formatWebpackMessages from '../../dev/error-overlay/format-webpack-messages' import { useRouter } from '../navigation' import { + ACTION_NOT_FOUND, ACTION_VERSION_INFO, INITIAL_OVERLAY_STATE, errorOverlayReducer, @@ -35,6 +36,8 @@ import { } from './internal/helpers/use-websocket' import { parseComponentStack } from './internal/helpers/parse-component-stack' import type { VersionInfo } from '../../../server/dev/parse-version-info' +import { isNotFoundError } from '../not-found' +import { NotFoundBoundary } from '../not-found-boundary' interface Dispatcher { onBuildOk(): void @@ -42,6 +45,7 @@ interface Dispatcher { onVersionInfo(versionInfo: VersionInfo): void onBeforeRefresh(): void onRefresh(): void + onNotFound(): void } // TODO-APP: add actual type @@ -50,6 +54,8 @@ type PongEvent = any let mostRecentCompilationHash: any = null let __nextDevClientId = Math.round(Math.random() * 100 + Date.now()) +// let startLatency = undefined + function onBeforeFastRefresh(dispatcher: Dispatcher, hasUpdates: boolean) { if (hasUpdates) { dispatcher.onBeforeRefresh() @@ -416,30 +422,18 @@ function processMessage( fetch(window.location.href, { credentials: 'same-origin', }).then((pageRes) => { - let shouldRefresh = pageRes.ok - // TODO-APP: investigate why edge runtime needs to reload - const isEdgeRuntime = pageRes.headers.get('x-edge-runtime') === '1' - if (pageRes.status === 404) { - // Check if head present as document.head could be null + if (pageRes.status === 200) { + // Page exists now, reload + startTransition(() => { + // @ts-ignore it exists, it's just hidden + router.fastRefresh() + dispatcher.onRefresh() + }) + } else if (pageRes.status === 404) { // We are still on the page, // dispatch an error so it's caught by the NotFound handler - const devErrorMetaTag = document.head?.querySelector( - 'meta[name="next-error"]' - ) - shouldRefresh = !devErrorMetaTag + dispatcher.onNotFound() } - // Page exists now, reload - startTransition(() => { - if (shouldRefresh) { - if (isEdgeRuntime) { - window.location.reload() - } else { - // @ts-ignore it exists, it's just hidden - router.fastRefresh() - dispatcher.onRefresh() - } - } - }) }) } return @@ -456,9 +450,15 @@ function processMessage( export default function HotReload({ assetPrefix, children, + notFound, + notFoundStyles, + asNotFound, }: { assetPrefix: string children?: ReactNode + notFound?: React.ReactNode + notFoundStyles?: React.ReactNode + asNotFound?: boolean }) { const [state, dispatch] = useReducer( errorOverlayReducer, @@ -481,6 +481,9 @@ export default function HotReload({ onVersionInfo(versionInfo) { dispatch({ type: ACTION_VERSION_INFO, versionInfo }) }, + onNotFound() { + dispatch({ type: ACTION_NOT_FOUND }) + }, } }, [dispatch]) @@ -502,7 +505,9 @@ export default function HotReload({ frames: parseStack(reason.stack!), }) }, []) - const handleOnReactError = useCallback(() => { + const handleOnReactError = useCallback((error: Error) => { + // not found errors are handled by the parent boundary, not the dev overlay + if (isNotFoundError(error)) throw error RuntimeErrorHandler.hadRuntimeError = true }, []) useErrorHandler(handleOnUnhandledError, handleOnUnhandledRejection) @@ -533,8 +538,15 @@ export default function HotReload({ }, [sendMessage, router, webSocketRef, dispatcher]) return ( - - {children} - + + + {children} + + ) } diff --git a/packages/next/src/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx b/packages/next/src/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx index 715e3fd1b9571..5b88f1b03c00f 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx +++ b/packages/next/src/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx @@ -13,6 +13,7 @@ import { parseStack } from './helpers/parseStack' import { Base } from './styles/Base' import { ComponentStyles } from './styles/ComponentStyles' import { CssReset } from './styles/CssReset' +import { notFound } from '../../not-found' interface ReactDevOverlayState { reactError: SupportedErrorEvent | null @@ -58,6 +59,10 @@ class ReactDevOverlay extends React.PureComponent< reactError || rootLayoutMissingTagsError + if (state.notFound) { + notFound() + } + return ( <> {reactError ? ( diff --git a/packages/next/src/client/components/react-dev-overlay/internal/error-overlay-reducer.ts b/packages/next/src/client/components/react-dev-overlay/internal/error-overlay-reducer.ts index 44cb2470db7a4..f9b44e7723c40 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/error-overlay-reducer.ts +++ b/packages/next/src/client/components/react-dev-overlay/internal/error-overlay-reducer.ts @@ -10,6 +10,7 @@ export const ACTION_REFRESH = 'fast-refresh' export const ACTION_UNHANDLED_ERROR = 'unhandled-error' export const ACTION_UNHANDLED_REJECTION = 'unhandled-rejection' export const ACTION_VERSION_INFO = 'version-info' +export const ACTION_NOT_FOUND = 'not-found' export const INITIAL_OVERLAY_STATE: OverlayState = { nextId: 1, buildError: null, @@ -33,6 +34,10 @@ interface FastRefreshAction { type: typeof ACTION_REFRESH } +interface NotFoundAction { + type: typeof ACTION_NOT_FOUND +} + export interface UnhandledErrorAction { type: typeof ACTION_UNHANDLED_ERROR reason: Error @@ -91,6 +96,7 @@ export const errorOverlayReducer: React.Reducer< | BuildErrorAction | BeforeFastRefreshAction | FastRefreshAction + | NotFoundAction | UnhandledErrorAction | UnhandledRejectionAction | VersionInfoAction @@ -98,7 +104,7 @@ export const errorOverlayReducer: React.Reducer< > = (state, action) => { switch (action.type) { case ACTION_BUILD_OK: { - return { ...state, buildError: null } + return { ...state, buildError: null, notFound: false } } case ACTION_BUILD_ERROR: { return { ...state, buildError: action.message } @@ -106,10 +112,14 @@ export const errorOverlayReducer: React.Reducer< case ACTION_BEFORE_REFRESH: { return { ...state, refreshState: { type: 'pending', errors: [] } } } + case ACTION_NOT_FOUND: { + return { ...state, notFound: true } + } case ACTION_REFRESH: { return { ...state, buildError: null, + notFound: false, errors: // Errors can come in during updates. In this case, UNHANDLED_ERROR // and UNHANDLED_REJECTION events might be dispatched between the diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 56acc6127cfbe..1a5b951c8a52d 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -17,7 +17,10 @@ import type { RequestAsyncStorage } from '../../client/components/request-async- import React from 'react' import { NotFound as DefaultNotFound } from '../../client/components/error' -import { createServerComponentRenderer } from './create-server-components-renderer' +import { + createServerComponentRenderer, + ErrorHtml, +} from './create-server-components-renderer' import { ParsedUrlQuery } from 'querystring' import { NextParsedUrlQuery } from '../request-meta' @@ -27,7 +30,6 @@ import { createBufferedTransformStream, continueFromInitialStream, streamToBufferedResult, - cloneTransformStream, } from '../stream-utils/node-web-streams-helper' import { canSegmentBeOverridden, @@ -79,6 +81,8 @@ import { appendMutableCookies } from '../web/spec-extension/adapters/request-coo import { ComponentsType } from '../../build/webpack/loaders/next-app-loader' import { ModuleReference } from '../../build/webpack/loaders/metadata/types' +export const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' + export type GetDynamicParamFromSegment = ( // [slug] / [[slug]] / [...slug] segment: string @@ -89,19 +93,6 @@ export type GetDynamicParamFromSegment = ( type: DynamicParamTypesShort } | null -function ErrorHtml({ - children, -}: { - head?: React.ReactNode - children?: React.ReactNode -}) { - return ( - - {children} - - ) -} - // Find the closest matched component in the loader tree for a given component type function findMatchedComponent( loaderTree: LoaderTree, @@ -614,7 +605,7 @@ export async function renderToHTMLOrFlight( firstItem?: boolean injectedCSS: Set injectedFontPreloadTags: Set - asNotFound?: boolean | 'force' + asNotFound?: boolean }): Promise<{ Component: React.ComponentType styles: React.ReactNode @@ -944,26 +935,12 @@ export async function renderToHTMLOrFlight( // If it's a not found route, and we don't have any matched parallel // routes, we try to render the not found component if it exists. - let isLeaf = - process.env.NODE_ENV === 'production' - ? !segment && !rootLayoutIncluded - : !parallelRouteMap.length && segment === '__DEFAULT__' // hit parallel-route-default - let notFoundComponent = {} - if ( - NotFound && - // For action not-found we force render the NotFound and stop checking the parallel routes. - (asNotFound === 'force' || - // For normal case where we should look up for not-found, keep checking the parallel routes. - (asNotFound && isLeaf)) - ) { + if (asNotFound && !parallelRouteMap.length && NotFound) { notFoundComponent = { children: ( <> - {process.env.NODE_ENV === 'development' && ( - - )} {notFoundStyles} @@ -1310,6 +1287,11 @@ export async function renderToHTMLOrFlight( Uint8Array > = new TransformStream() + const serverErrorComponentsInlinedTransformStream: TransformStream< + Uint8Array, + Uint8Array + > = new TransformStream() + // Get the nonce from the incoming request if it has one. const csp = req.headers['content-security-policy'] let nonce: string | undefined @@ -1324,6 +1306,13 @@ export async function renderToHTMLOrFlight( rscChunks: [], } + const serverErrorComponentsRenderOpts = { + transformStream: serverErrorComponentsInlinedTransformStream, + clientReferenceManifest, + serverContexts, + rscChunks: [], + } + const validateRootLayout = dev ? { validateRootLayout: { @@ -1343,47 +1332,32 @@ export async function renderToHTMLOrFlight( injectedCSS: Set, requestPathname: string ) { + const { layout } = tree[2] // `depth` represents how many layers we need to search into the tree. // For instance: // pathname '/abc' will be 0 depth, means stop at the root level // pathname '/abc/def' will be 1 depth, means stop at the first level const depth = requestPathname.split('/').length - 2 const notFound = findMatchedComponent(tree, 'not-found', depth) + const rootLayoutAtThisLevel = typeof layout !== 'undefined' const [NotFound, notFoundStyles] = notFound ? await createComponentAndStyles({ filePath: notFound[1], getComponent: notFound[0], injectedCSS, }) + : rootLayoutAtThisLevel + ? [DefaultNotFound] : [] return [NotFound, notFoundStyles] } - async function getRootLayout( - tree: LoaderTree, - injectedCSS: Set, - injectedFontPreloadTags: Set - ) { - const { layout } = tree[2] - const layoutPath = layout?.[1] - const styles = getLayerAssets({ - layoutOrPagePath: layoutPath, - injectedCSS: new Set(injectedCSS), - injectedFontPreloadTags: new Set(injectedFontPreloadTags), - }) - const rootLayoutModule = layout?.[0] - const RootLayout = rootLayoutModule - ? interopDefault(await rootLayoutModule()) - : null - return [RootLayout, styles] - } - /** * A new React Component that renders the provided React Component * using Flight which can then be rendered to HTML. */ const ServerComponentsRenderer = createServerComponentRenderer<{ - asNotFound: boolean | 'force' + asNotFound: boolean }>( async (props) => { // Create full component tree from root to leaf. @@ -1401,6 +1375,12 @@ export async function renderToHTMLOrFlight( asNotFound: props.asNotFound, }) + const initialTree = createFlightRouterStateFromLoaderTree( + loaderTree, + getDynamicParamFromSegment, + query + ) + const createMetadata = (tree: LoaderTree, errorType?: 'not-found') => ( // Adding key={requestId} to make metadata remount for each render // @ts-expect-error allow to use async server component @@ -1415,10 +1395,10 @@ export async function renderToHTMLOrFlight( /> ) - const initialTree = createFlightRouterStateFromLoaderTree( + const [NotFound, notFoundStyles] = await getNotFound( loaderTree, - getDynamicParamFromSegment, - query + injectedCSS, + pathname ) return ( @@ -1429,11 +1409,18 @@ export async function renderToHTMLOrFlight( assetPrefix={assetPrefix} initialCanonicalUrl={pathname} initialTree={initialTree} - initialHead={createMetadata( - loaderTree, - props.asNotFound ? 'not-found' : undefined - )} + initialHead={<>{createMetadata(loaderTree, undefined)}} globalErrorComponent={GlobalError} + notFound={ + NotFound ? ( + + {createMetadata(loaderTree, 'not-found')} + {notFoundStyles} + + + ) : undefined + } + asNotFound={props.asNotFound} > @@ -1488,10 +1475,8 @@ export async function renderToHTMLOrFlight( * This option is used to indicate that the page should be rendered as * if it was not found. When it's enabled, instead of rendering the * page component, it renders the not-found segment. - * - * If it's 'force', we don't traverse the tree and directly render the NotFound. */ - asNotFound: boolean | 'force' + asNotFound?: boolean }) => { const polyfills = buildManifest.polyfillFiles .filter( @@ -1507,7 +1492,7 @@ export async function renderToHTMLOrFlight( const content = ( - + ) @@ -1523,17 +1508,9 @@ export async function renderToHTMLOrFlight( flushedErrorMetaTagsUntilIndex++ ) { const error = serverCapturedErrors[flushedErrorMetaTagsUntilIndex] - if (isNotFoundError(error)) { errorMetaTags.push( - , - process.env.NODE_ENV === 'development' ? ( - - ) : null + ) } else if (isRedirectError(error)) { const redirectUrl = getURLFromRedirectError(error) @@ -1609,7 +1586,7 @@ export async function renderToHTMLOrFlight( }) const result = await continueFromInitialStream(renderStream, { - dataStream: serverComponentsRenderOpts.transformStream.readable, + dataStream: serverComponentsInlinedTransformStream.readable, generateStaticHTML: staticGenerationStore.isStaticGeneration || generateStaticHTML, getServerInsertedHTML: () => @@ -1635,7 +1612,6 @@ export async function renderToHTMLOrFlight( pagePath ) } - if (isNotFoundError(err)) { res.statusCode = 404 } @@ -1655,154 +1631,104 @@ export async function renderToHTMLOrFlight( res.setHeader('Location', getURLFromRedirectError(err)) } - const is404 = res.statusCode === 404 + const use404Error = res.statusCode === 404 + const useDefaultError = res.statusCode < 400 || hasRedirectError + const { layout } = loaderTree[2] const injectedCSS = new Set() - const injectedFontPreloadTags = new Set() - const [RootLayout, rootStyles] = await getRootLayout( - loaderTree, - injectedCSS, - injectedFontPreloadTags - ) const [NotFound, notFoundStyles] = await getNotFound( loaderTree, injectedCSS, pathname ) - // Preserve the existing RSC inline chunks from the page rendering. - // For 404 errors: the metadata from layout can be skipped with the error page. - // For other errors (such as redirection): it can still be re-thrown on client. - const serverErrorComponentsRenderOpts: typeof serverComponentsRenderOpts = - { - ...serverComponentsRenderOpts, - rscChunks: [], - transformStream: is404 - ? new TransformStream() - : cloneTransformStream( - serverComponentsRenderOpts.transformStream - ), - } - - const errorType = is404 - ? 'not-found' - : hasRedirectError - ? 'redirect' - : undefined - - const errorMeta = ( - <> - {res.statusCode >= 400 && ( - - )} - {process.env.NODE_ENV === 'development' && ( - - )} - + const rootLayoutModule = layout?.[0] + const RootLayout = rootLayoutModule + ? interopDefault(await rootLayoutModule()) + : null + + const metadata = ( + // @ts-expect-error allow to use async server component + ) - const ErrorPage = createServerComponentRenderer( - async () => { - const head = ( - <> - {/* @ts-expect-error allow to use async server component */} - - {errorMeta} - - ) - - const notFoundLoaderTree: LoaderTree = is404 - ? ['__DEFAULT__', {}, loaderTree[2]] - : loaderTree - - const initialTree = createFlightRouterStateFromLoaderTree( - notFoundLoaderTree, - getDynamicParamFromSegment, - query - ) - - const GlobalNotFound = NotFound || DefaultNotFound - const ErrorLayout = RootLayout || ErrorHtml - - const notFoundElement = ( - - {rootStyles} - {notFoundStyles} - - - ) - - // For metadata notFound error there's no global not found boundary on top - // so we create a not found page with AppRouter - return ( - - {is404 ? notFoundElement : } - - ) - }, - ComponentMod, - serverErrorComponentsRenderOpts, - serverComponentsErrorHandler, - nonce + const serverErrorElement = ( + + {useDefaultError + ? null + : React.createElement( + createServerComponentRenderer( + async () => { + return ( + <> + {/* For server components error metadata needs to be inside inline flight data, so they can be hydrated */} + {metadata} + {use404Error ? ( + + {notFoundStyles} + + + + ) : undefined} + + ) + }, + ComponentMod, + serverErrorComponentsRenderOpts, + serverComponentsErrorHandler, + nonce + ) + )} + ) - try { - const renderStream = await renderToInitialStream({ - ReactDOMServer: require('react-dom/server.edge'), - element: , - streamOptions: { - nonce, - // Include hydration scripts in the HTML - bootstrapScripts: subresourceIntegrityManifest - ? buildManifest.rootMainFiles.map((src) => ({ - src: - `${assetPrefix}/_next/` + - src + - getAssetQueryString(false), - integrity: subresourceIntegrityManifest[src], - })) - : buildManifest.rootMainFiles.map( - (src) => - `${assetPrefix}/_next/` + - src + - getAssetQueryString(false) - ), - }, - }) + const renderStream = await renderToInitialStream({ + ReactDOMServer: require('react-dom/server.edge'), + element: serverErrorElement, + streamOptions: { + nonce, + // Include hydration scripts in the HTML + bootstrapScripts: subresourceIntegrityManifest + ? buildManifest.rootMainFiles.map((src) => ({ + src: + `${assetPrefix}/_next/` + + src + + getAssetQueryString(false), + integrity: subresourceIntegrityManifest[src], + })) + : buildManifest.rootMainFiles.map( + (src) => + `${assetPrefix}/_next/` + src + getAssetQueryString(false) + ), + }, + }) - return await continueFromInitialStream(renderStream, { - dataStream: - serverErrorComponentsRenderOpts.transformStream.readable, - generateStaticHTML: staticGenerationStore.isStaticGeneration, - getServerInsertedHTML: () => getServerInsertedHTML([]), - serverInsertedHTMLToHead: true, - ...validateRootLayout, - }) - } catch (finalErr: any) { - if ( - process.env.NODE_ENV !== 'production' && - isNotFoundError(finalErr) - ) { - const bailOnNotFound: typeof import('../../client/components/dev-root-not-found-boundary').bailOnNotFound = - require('../../client/components/dev-root-not-found-boundary').bailOnNotFound - bailOnNotFound() - } - throw finalErr - } + return await continueFromInitialStream(renderStream, { + dataStream: (useDefaultError + ? serverComponentsInlinedTransformStream + : serverErrorComponentsInlinedTransformStream + ).readable, + generateStaticHTML: staticGenerationStore.isStaticGeneration, + getServerInsertedHTML: () => getServerInsertedHTML([]), + serverInsertedHTMLToHead: true, + ...validateRootLayout, + }) } } ) @@ -1821,7 +1747,7 @@ export async function renderToHTMLOrFlight( }) if (actionRequestResult === 'not-found') { - return new RenderResult(await bodyResult({ asNotFound: 'force' })) + return new RenderResult(await bodyResult({ asNotFound: true })) } else if (actionRequestResult) { return actionRequestResult } diff --git a/packages/next/src/server/app-render/create-server-components-renderer.tsx b/packages/next/src/server/app-render/create-server-components-renderer.tsx index 0d11231ed110d..5fdc1008bf819 100644 --- a/packages/next/src/server/app-render/create-server-components-renderer.tsx +++ b/packages/next/src/server/app-render/create-server-components-renderer.tsx @@ -75,3 +75,18 @@ export function createServerComponentRenderer( return use(response) } } + +export function ErrorHtml({ + head, + children, +}: { + head?: React.ReactNode + children?: React.ReactNode +}) { + return ( + + {head} + {children} + + ) +} diff --git a/packages/next/src/server/lib/app-dir-module.ts b/packages/next/src/server/lib/app-dir-module.ts index cc07efeff5321..02e735a3edf7a 100644 --- a/packages/next/src/server/lib/app-dir-module.ts +++ b/packages/next/src/server/lib/app-dir-module.ts @@ -36,7 +36,7 @@ export async function getLayoutOrPageModule(loaderTree: LoaderTree) { // First check not-found, if it doesn't exist then pick layout export async function getErrorOrLayoutModule( loaderTree: LoaderTree, - errorType: 'not-found' + errorType: 'error' | 'not-found' ) { const { [errorType]: error, layout } = loaderTree[2] if (typeof error !== 'undefined') { diff --git a/packages/next/src/server/render.tsx b/packages/next/src/server/render.tsx index aab167b244f3c..1e1ca4da11397 100644 --- a/packages/next/src/server/render.tsx +++ b/packages/next/src/server/render.tsx @@ -457,6 +457,7 @@ export async function renderToHTMLImpl( let Document = extra.Document + // Component will be wrapped by ServerComponentWrapper for RSC let Component: React.ComponentType<{}> | ((props: any) => JSX.Element) = renderOpts.Component const OriginComponent = Component diff --git a/packages/next/src/server/stream-utils/node-web-streams-helper.ts b/packages/next/src/server/stream-utils/node-web-streams-helper.ts index 6b230596a4d9a..7f6312965902e 100644 --- a/packages/next/src/server/stream-utils/node-web-streams-helper.ts +++ b/packages/next/src/server/stream-utils/node-web-streams-helper.ts @@ -30,25 +30,6 @@ export const streamToBufferedResult = async ( return renderChunks.join('') } -export function cloneTransformStream(source: TransformStream) { - const sourceReader = source.readable.getReader() - const clone = new TransformStream({ - async start(controller) { - while (true) { - const { done, value } = await sourceReader.read() - if (done) { - break - } - controller.enqueue(value) - } - }, - // skip the its own written chunks - transform() {}, - }) - - return clone -} - export function readableStreamTee( readable: ReadableStream ): [ReadableStream, ReadableStream] { diff --git a/test/e2e/app-dir/actions/app/server/client-form.js b/test/e2e/app-dir/actions/app/server/client-form.js index 6c940e8974031..676adf93a9669 100644 --- a/test/e2e/app-dir/actions/app/server/client-form.js +++ b/test/e2e/app-dir/actions/app/server/client-form.js @@ -5,7 +5,7 @@ import { redirectAction } from './actions' export default function Form() { return (
- + - {children} - - - ) -} - -export const dynamic = 'force-dynamic' diff --git a/test/e2e/app-dir/root-layout-not-found/app/not-found-trigger.js b/test/e2e/app-dir/root-layout-not-found/app/not-found-trigger.js deleted file mode 100644 index 4fa8d4cba0d40..0000000000000 --- a/test/e2e/app-dir/root-layout-not-found/app/not-found-trigger.js +++ /dev/null @@ -1,12 +0,0 @@ -'use client' - -import { useSearchParams, notFound } from 'next/navigation' - -export default function NotFoundTrigger() { - const searchParams = useSearchParams() - - if (searchParams.get('root-not-found')) { - notFound() - } - return null -} diff --git a/test/e2e/app-dir/root-layout-not-found/app/page.js b/test/e2e/app-dir/root-layout-not-found/app/page.js deleted file mode 100644 index ff7159d9149fe..0000000000000 --- a/test/e2e/app-dir/root-layout-not-found/app/page.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return

hello world

-} diff --git a/test/e2e/app-dir/root-layout-not-found/index.test.ts b/test/e2e/app-dir/root-layout-not-found/index.test.ts deleted file mode 100644 index e3e86a78d64da..0000000000000 --- a/test/e2e/app-dir/root-layout-not-found/index.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { createNextDescribe } from 'e2e-utils' -import { check, getRedboxDescription, hasRedbox } from 'next-test-utils' - -createNextDescribe( - 'app dir - root layout not found', - { - files: __dirname, - skipDeployment: true, - }, - ({ next, isNextDev }) => { - it('should error on client notFound from root layout in browser', async () => { - const browser = await next.browser('/') - - await browser.elementByCss('#trigger-not-found').click() - - if (isNextDev) { - await check(async () => { - expect(await hasRedbox(browser, true)).toBe(true) - expect(await getRedboxDescription(browser)).toMatch( - /notFound\(\) is not allowed to use in root layout/ - ) - return 'success' - }, /success/) - } else { - expect(await browser.elementByCss('h2').text()).toBe( - 'Application error: a server-side exception has occurred (see the server logs for more information).' - ) - expect(await browser.elementByCss('p').text()).toBe( - 'Digest: NEXT_NOT_FOUND' - ) - } - }) - - it('should error on server notFound from root layout on server-side', async () => { - const browser = await next.browser('/?root-not-found=1') - - if (isNextDev) { - expect(await hasRedbox(browser, true)).toBe(true) - expect(await getRedboxDescription(browser)).toBe( - 'Error: notFound() is not allowed to use in root layout' - ) - } else { - expect(await browser.elementByCss('h2').text()).toBe( - 'Application error: a server-side exception has occurred (see the server logs for more information).' - ) - expect(await browser.elementByCss('p').text()).toBe( - 'Digest: NEXT_NOT_FOUND' - ) - } - }) - } -)