Skip to content

Commit dcece12

Browse files
authored
[errors] revamp graceful degrade error boundary (#82474)
1 parent e30dd09 commit dcece12

File tree

7 files changed

+59
-76
lines changed

7 files changed

+59
-76
lines changed

packages/next/src/client/app-index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import type { InitialRSCPayload } from '../server/app-render/types'
2121
import { createInitialRouterState } from './components/router-reducer/create-initial-router-state'
2222
import { MissingSlotContext } from '../shared/lib/app-router-context.shared-runtime'
2323
import { setAppBuildId } from './app-build-id'
24-
import { isBot } from '../shared/lib/router/utils/is-bot'
2524

2625
/// <reference types="react-dom/experimental" />
2726

@@ -169,7 +168,6 @@ function ServerRoot({
169168

170169
const router = (
171170
<AppRouter
172-
gracefullyDegrade={isBot(window.navigator.userAgent)}
173171
actionQueue={actionQueue}
174172
globalErrorState={initialRSCPayload.G}
175173
assetPrefix={initialRSCPayload.p}

packages/next/src/client/components/app-router.tsx

Lines changed: 17 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import {
2222
PathParamsContext,
2323
} from '../../shared/lib/hooks-client-context.shared-runtime'
2424
import { dispatchAppRouterAction, useActionQueue } from './use-action-queue'
25-
import { ErrorBoundary } from './error-boundary'
26-
import DefaultGlobalError from './builtin/global-error'
2725
import { isBot } from '../../shared/lib/router/utils/is-bot'
2826
import { addBasePath } from '../add-base-path'
2927
import { AppRouterAnnouncer } from './app-router-announcer'
@@ -44,7 +42,8 @@ import {
4442
import { getRedirectTypeFromError, getURLFromRedirectError } from './redirect'
4543
import { isRedirectError, RedirectType } from './redirect-error'
4644
import { pingVisibleLinks } from './links'
47-
import GracefulDegradeBoundary from './errors/graceful-degrade-boundary'
45+
import RootErrorBoundary from './errors/root-error-boundary'
46+
import DefaultGlobalError from './builtin/global-error'
4847

4948
const globalMutable: {
5049
pendingMpaPath?: string
@@ -196,12 +195,10 @@ function Router({
196195
actionQueue,
197196
assetPrefix,
198197
globalError,
199-
gracefullyDegrade,
200198
}: {
201199
actionQueue: AppRouterActionQueue
202200
assetPrefix: string
203201
globalError: GlobalErrorState
204-
gracefullyDegrade: boolean
205202
}) {
206203
const state = useActionQueue(actionQueue)
207204
const { canonicalUrl } = state
@@ -517,20 +514,14 @@ function Router({
517514
</HotReloader>
518515
)
519516
} else {
520-
// If gracefully degrading is applied in production,
521-
// leave the app as it is rather than caught by GlobalError boundary.
522-
if (gracefullyDegrade) {
523-
content = <GracefulDegradeBoundary>{content}</GracefulDegradeBoundary>
524-
} else {
525-
content = (
526-
<ErrorBoundary
527-
errorComponent={globalError[0]}
528-
errorStyles={globalError[1]}
529-
>
530-
{content}
531-
</ErrorBoundary>
532-
)
533-
}
517+
content = (
518+
<RootErrorBoundary
519+
errorComponent={globalError[0]}
520+
errorStyles={globalError[1]}
521+
>
522+
{content}
523+
</RootErrorBoundary>
524+
)
534525
}
535526

536527
return (
@@ -565,12 +556,10 @@ export default function AppRouter({
565556
actionQueue,
566557
globalErrorState,
567558
assetPrefix,
568-
gracefullyDegrade,
569559
}: {
570560
actionQueue: AppRouterActionQueue
571561
globalErrorState: GlobalErrorState
572562
assetPrefix: string
573-
gracefullyDegrade: boolean
574563
}) {
575564
useNavFailureHandler()
576565

@@ -579,23 +568,16 @@ export default function AppRouter({
579568
actionQueue={actionQueue}
580569
assetPrefix={assetPrefix}
581570
globalError={globalErrorState}
582-
gracefullyDegrade={gracefullyDegrade}
583571
/>
584572
)
585573

586-
if (gracefullyDegrade) {
587-
return router
588-
} else {
589-
return (
590-
<ErrorBoundary
591-
// At the very top level, use the default GlobalError component as the final fallback.
592-
// When the app router itself fails, which means the framework itself fails, we show the default error.
593-
errorComponent={DefaultGlobalError}
594-
>
595-
{router}
596-
</ErrorBoundary>
597-
)
598-
}
574+
// At the very top level, use the default GlobalError component as the final fallback.
575+
// When the app router itself fails, which means the framework itself fails, we show the default error.
576+
return (
577+
<RootErrorBoundary errorComponent={DefaultGlobalError}>
578+
{router}
579+
</RootErrorBoundary>
580+
)
599581
}
600582

601583
const runtimeStyles = new Set<string>()

packages/next/src/client/components/error-boundary.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { useUntrackedPathname } from './navigation-untracked'
55
import { isNextRouterError } from './is-next-router-error'
66
import { handleHardNavError } from './nav-failure-handler'
77
import { HandleISRError } from './handle-isr-error'
8+
import { isBot } from '../../shared/lib/router/utils/is-bot'
9+
10+
const isBotUserAgent =
11+
typeof window !== 'undefined' && isBot(window.navigator.userAgent)
812

913
export type ErrorComponent = React.ComponentType<{
1014
error: Error
@@ -93,7 +97,9 @@ export class ErrorBoundaryHandler extends React.Component<
9397

9498
// Explicit type is needed to avoid the generated `.d.ts` having a wide return type that could be specific to the `@types/react` version.
9599
render(): React.ReactNode {
96-
if (this.state.error) {
100+
//When it's bot request, segment level error boundary will keep rendering the children,
101+
// the final error will be caught by the root error boundary and determine wether need to apply graceful degrade.
102+
if (this.state.error && !isBotUserAgent) {
97103
return (
98104
<>
99105
<HandleISRError error={this.state.error} />
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use client'
2+
3+
import React, { type JSX } from 'react'
4+
import GracefulDegradeBoundary from './graceful-degrade-boundary'
5+
import { ErrorBoundary, type ErrorBoundaryProps } from '../error-boundary'
6+
import { isBot } from '../../../shared/lib/router/utils/is-bot'
7+
8+
const isBotUserAgent =
9+
typeof window !== 'undefined' && isBot(window.navigator.userAgent)
10+
11+
export default function RootErrorBoundary({
12+
children,
13+
errorComponent,
14+
errorStyles,
15+
errorScripts,
16+
}: ErrorBoundaryProps & { children: React.ReactNode }): JSX.Element {
17+
if (isBotUserAgent) {
18+
// Preserve existing DOM/HTML for bots to avoid replacing content with an error UI
19+
// and to keep the original SSR output intact.
20+
return <GracefulDegradeBoundary>{children}</GracefulDegradeBoundary>
21+
}
22+
23+
return (
24+
<ErrorBoundary
25+
errorComponent={errorComponent}
26+
errorStyles={errorStyles}
27+
errorScripts={errorScripts}
28+
>
29+
{children}
30+
</ErrorBoundary>
31+
)
32+
}

packages/next/src/client/components/layout-router.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -488,10 +488,6 @@ function LoadingBoundary({
488488
return <>{children}</>
489489
}
490490

491-
function RenderChildren({ children }: { children: React.ReactNode }) {
492-
return <>{children}</>
493-
}
494-
495491
/**
496492
* OuterLayoutRouter handles the current segment as well as <Offscreen> rendering of other segments.
497493
* It can be rendered next to each other with a different `parallelRouterKey`, allowing for Parallel routes.
@@ -507,7 +503,6 @@ export default function OuterLayoutRouter({
507503
notFound,
508504
forbidden,
509505
unauthorized,
510-
gracefullyDegrade,
511506
segmentViewBoundaries,
512507
}: {
513508
parallelRouterKey: string
@@ -520,7 +515,6 @@ export default function OuterLayoutRouter({
520515
notFound: React.ReactNode | undefined
521516
forbidden: React.ReactNode | undefined
522517
unauthorized: React.ReactNode | undefined
523-
gracefullyDegrade?: boolean
524518
segmentViewBoundaries?: React.ReactNode
525519
}) {
526520
const context = useContext(LayoutRouterContext)
@@ -612,10 +606,6 @@ export default function OuterLayoutRouter({
612606
- Passed to the router during rendering to ensure it can be immediately rendered when suspending on a Flight fetch.
613607
*/
614608

615-
const ErrorBoundaryComponent = gracefullyDegrade
616-
? RenderChildren
617-
: ErrorBoundary
618-
619609
let segmentBoundaryTriggerNode: React.ReactNode = null
620610
let segmentViewStateNode: React.ReactNode = null
621611
if (
@@ -651,7 +641,7 @@ export default function OuterLayoutRouter({
651641
key={stateKey}
652642
value={
653643
<ScrollAndFocusHandler segmentPath={segmentPath}>
654-
<ErrorBoundaryComponent
644+
<ErrorBoundary
655645
errorComponent={error}
656646
errorStyles={errorStyles}
657647
errorScripts={errorScripts}
@@ -673,7 +663,7 @@ export default function OuterLayoutRouter({
673663
</RedirectBoundary>
674664
</HTTPAccessFallbackBoundary>
675665
</LoadingBoundary>
676-
</ErrorBoundaryComponent>
666+
</ErrorBoundary>
677667
{segmentViewStateNode}
678668
</ScrollAndFocusHandler>
679669
}

packages/next/src/server/app-render/app-render.tsx

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,14 +1309,12 @@ function App<T>({
13091309
preinitScripts,
13101310
clientReferenceManifest,
13111311
ServerInsertedHTMLProvider,
1312-
gracefullyDegrade,
13131312
nonce,
13141313
}: {
13151314
reactServerStream: BinaryStreamOf<T>
13161315
preinitScripts: () => void
13171316
clientReferenceManifest: NonNullable<RenderOpts['clientReferenceManifest']>
13181317
ServerInsertedHTMLProvider: React.ComponentType<{ children: JSX.Element }>
1319-
gracefullyDegrade: boolean
13201318
nonce?: string
13211319
}): JSX.Element {
13221320
preinitScripts()
@@ -1360,7 +1358,6 @@ function App<T>({
13601358
actionQueue={actionQueue}
13611359
globalErrorState={response.G}
13621360
assetPrefix={response.p}
1363-
gracefullyDegrade={gracefullyDegrade}
13641361
/>
13651362
</ServerInsertedHTMLProvider>
13661363
</HeadManagerContext.Provider>
@@ -1375,14 +1372,12 @@ function ErrorApp<T>({
13751372
preinitScripts,
13761373
clientReferenceManifest,
13771374
ServerInsertedHTMLProvider,
1378-
gracefullyDegrade,
13791375
nonce,
13801376
}: {
13811377
reactServerStream: BinaryStreamOf<T>
13821378
preinitScripts: () => void
13831379
clientReferenceManifest: NonNullable<RenderOpts['clientReferenceManifest']>
13841380
ServerInsertedHTMLProvider: React.ComponentType<{ children: JSX.Element }>
1385-
gracefullyDegrade: boolean
13861381
nonce?: string
13871382
}): JSX.Element {
13881383
preinitScripts()
@@ -1417,7 +1412,6 @@ function ErrorApp<T>({
14171412
actionQueue={actionQueue}
14181413
globalErrorState={response.G}
14191414
assetPrefix={response.p}
1420-
gracefullyDegrade={gracefullyDegrade}
14211415
/>
14221416
</ServerInsertedHTMLProvider>
14231417
)
@@ -2070,7 +2064,6 @@ async function renderToStream(
20702064

20712065
const {
20722066
basePath,
2073-
botType,
20742067
buildManifest,
20752068
clientReferenceManifest,
20762069
ComponentMod,
@@ -2289,7 +2282,6 @@ async function renderToStream(
22892282
clientReferenceManifest={clientReferenceManifest}
22902283
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
22912284
nonce={nonce}
2292-
gracefullyDegrade={!!botType}
22932285
/>,
22942286
postponed,
22952287
{ onError: htmlRendererErrorHandler, nonce }
@@ -2327,7 +2319,6 @@ async function renderToStream(
23272319
preinitScripts={preinitScripts}
23282320
clientReferenceManifest={clientReferenceManifest}
23292321
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
2330-
gracefullyDegrade={!!botType}
23312322
nonce={nonce}
23322323
/>,
23332324
{
@@ -2486,7 +2477,6 @@ async function renderToStream(
24862477
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
24872478
preinitScripts={errorPreinitScripts}
24882479
clientReferenceManifest={clientReferenceManifest}
2489-
gracefullyDegrade={!!botType}
24902480
nonce={nonce}
24912481
/>
24922482
),
@@ -2585,7 +2575,7 @@ async function spawnDynamicValidationInDev(
25852575
workStore,
25862576
} = ctx
25872577

2588-
const { allowEmptyStaticShell = false, botType } = renderOpts
2578+
const { allowEmptyStaticShell = false } = renderOpts
25892579

25902580
// These values are placeholder values for this validating render
25912581
// that are provided during the actual prerenderToStream.
@@ -2830,7 +2820,6 @@ async function spawnDynamicValidationInDev(
28302820
preinitScripts={preinitScripts}
28312821
clientReferenceManifest={clientReferenceManifest}
28322822
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
2833-
gracefullyDegrade={!!botType}
28342823
nonce={nonce}
28352824
/>,
28362825
{
@@ -3059,7 +3048,6 @@ async function spawnDynamicValidationInDev(
30593048
preinitScripts={preinitScripts}
30603049
clientReferenceManifest={clientReferenceManifest}
30613050
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
3062-
gracefullyDegrade={!!botType}
30633051
nonce={nonce}
30643052
/>,
30653053
{
@@ -3208,7 +3196,6 @@ async function prerenderToStream(
32083196
const {
32093197
allowEmptyStaticShell = false,
32103198
basePath,
3211-
botType,
32123199
buildManifest,
32133200
clientReferenceManifest,
32143201
ComponentMod,
@@ -3573,7 +3560,6 @@ async function prerenderToStream(
35733560
preinitScripts={preinitScripts}
35743561
clientReferenceManifest={clientReferenceManifest}
35753562
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
3576-
gracefullyDegrade={!!botType}
35773563
nonce={nonce}
35783564
/>,
35793565
{
@@ -3807,7 +3793,6 @@ async function prerenderToStream(
38073793
preinitScripts={preinitScripts}
38083794
clientReferenceManifest={clientReferenceManifest}
38093795
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
3810-
gracefullyDegrade={!!botType}
38113796
nonce={nonce}
38123797
/>,
38133798
{
@@ -3965,7 +3950,6 @@ async function prerenderToStream(
39653950
preinitScripts={() => {}}
39663951
clientReferenceManifest={clientReferenceManifest}
39673952
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
3968-
gracefullyDegrade={!!botType}
39693953
nonce={nonce}
39703954
/>,
39713955
JSON.parse(JSON.stringify(postponed)),
@@ -4071,7 +4055,6 @@ async function prerenderToStream(
40714055
preinitScripts={preinitScripts}
40724056
clientReferenceManifest={clientReferenceManifest}
40734057
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
4074-
gracefullyDegrade={!!botType}
40754058
nonce={nonce}
40764059
/>,
40774060
{
@@ -4203,7 +4186,6 @@ async function prerenderToStream(
42034186
preinitScripts={() => {}}
42044187
clientReferenceManifest={clientReferenceManifest}
42054188
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
4206-
gracefullyDegrade={!!botType}
42074189
nonce={nonce}
42084190
/>,
42094191
JSON.parse(JSON.stringify(postponed)),
@@ -4287,7 +4269,6 @@ async function prerenderToStream(
42874269
preinitScripts={preinitScripts}
42884270
clientReferenceManifest={clientReferenceManifest}
42894271
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
4290-
gracefullyDegrade={!!botType}
42914272
nonce={nonce}
42924273
/>,
42934274
{
@@ -4461,7 +4442,6 @@ async function prerenderToStream(
44614442
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
44624443
preinitScripts={errorPreinitScripts}
44634444
clientReferenceManifest={clientReferenceManifest}
4464-
gracefullyDegrade={!!botType}
44654445
nonce={nonce}
44664446
/>
44674447
),

0 commit comments

Comments
 (0)