From 858f563271b5e262d0b1f48822a896092987cd96 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Tue, 2 Jan 2024 13:14:28 -0800 Subject: [PATCH 1/2] gracefully handle client router segment mismatches --- .../router-reducer/handle-segment-mismatch.ts | 28 +++++++++ .../reducers/fast-refresh-reducer.ts | 3 +- .../reducers/refresh-reducer.ts | 3 +- .../reducers/server-action-reducer.ts | 3 +- .../reducers/server-patch-reducer.test.tsx | 58 +++++++++++++++++++ .../reducers/server-patch-reducer.ts | 3 +- 6 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 packages/next/src/client/components/router-reducer/handle-segment-mismatch.ts diff --git a/packages/next/src/client/components/router-reducer/handle-segment-mismatch.ts b/packages/next/src/client/components/router-reducer/handle-segment-mismatch.ts new file mode 100644 index 0000000000000..0ef744f18f255 --- /dev/null +++ b/packages/next/src/client/components/router-reducer/handle-segment-mismatch.ts @@ -0,0 +1,28 @@ +import type { FlightRouterState } from '../../../server/app-render/types' +import { handleExternalUrl } from './reducers/navigate-reducer' +import type { + ReadonlyReducerState, + ReducerActions, +} from './router-reducer-types' + +/** + * Handles the case where the client router attempted to patch the tree but, due to a mismatch, the patch failed. + * This will perform an MPA navigation to return the router to a valid state. + */ +export function handleSegmentMismatch( + state: ReadonlyReducerState, + action: ReducerActions, + treePatch: FlightRouterState +) { + if (process.env.NODE_ENV === 'development') { + console.warn( + 'Performing full reload because your application experienced an unrecoverable error. If this keeps occurring, please file a Next.js issue.\n\n' + + 'Reason: Segment mismatch\n' + + `Last Action: ${action.type}\n\n` + + `Current Tree: ${JSON.stringify(state.tree)}\n\n` + + `Tree Patch Payload: ${JSON.stringify(treePatch)}` + ) + } + + return handleExternalUrl(state, {}, state.canonicalUrl, true) +} diff --git a/packages/next/src/client/components/router-reducer/reducers/fast-refresh-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/fast-refresh-reducer.ts index 4bb8219421472..127da795505ab 100644 --- a/packages/next/src/client/components/router-reducer/reducers/fast-refresh-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/fast-refresh-reducer.ts @@ -13,6 +13,7 @@ import { handleMutable } from '../handle-mutable' import { applyFlightData } from '../apply-flight-data' import type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime' import { createEmptyCacheNode } from '../../app-router' +import { handleSegmentMismatch } from '../handle-segment-mismatch' // A version of refresh reducer that keeps the cache around instead of wiping all of it. function fastRefreshReducerImpl( @@ -71,7 +72,7 @@ function fastRefreshReducerImpl( ) if (newTree === null) { - throw new Error('SEGMENT MISMATCH') + return handleSegmentMismatch(state, action, treePatch) } if (isNavigatingToNewRootLayout(currentTree, newTree)) { diff --git a/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts index e6b0c0b54797c..d5a0bb15c95cd 100644 --- a/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts @@ -13,6 +13,7 @@ import { handleMutable } from '../handle-mutable' import type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime' import { fillLazyItemsTillLeafWithHead } from '../fill-lazy-items-till-leaf-with-head' import { createEmptyCacheNode } from '../../app-router' +import { handleSegmentMismatch } from '../handle-segment-mismatch' export function refreshReducer( state: ReadonlyReducerState, @@ -69,7 +70,7 @@ export function refreshReducer( ) if (newTree === null) { - throw new Error('SEGMENT MISMATCH') + return handleSegmentMismatch(state, action, treePatch) } if (isNavigatingToNewRootLayout(currentTree, newTree)) { diff --git a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts index fd6433f4b2114..912b172c6e360 100644 --- a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts @@ -38,6 +38,7 @@ import { handleMutable } from '../handle-mutable' import { fillLazyItemsTillLeafWithHead } from '../fill-lazy-items-till-leaf-with-head' import { createEmptyCacheNode } from '../../app-router' import { extractPathFromFlightRouterState } from '../compute-changed-path' +import { handleSegmentMismatch } from '../handle-segment-mismatch' type FetchServerActionResult = { redirectLocation: URL | undefined @@ -224,7 +225,7 @@ export function serverActionReducer( ) if (newTree === null) { - throw new Error('SEGMENT MISMATCH') + return handleSegmentMismatch(state, action, treePatch) } if (isNavigatingToNewRootLayout(currentTree, newTree)) { diff --git a/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.test.tsx b/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.test.tsx index 171cbd0503bdd..47773163997aa 100644 --- a/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.test.tsx +++ b/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.test.tsx @@ -501,4 +501,62 @@ describe('serverPatchReducer', () => { } `) }) + + it("should gracefully recover if the server patch doesn't match the current tree", async () => { + const initialTree = getInitialRouterStateTree() + const initialCanonicalUrl = '/linking' + const children = ( + + + Root layout + + ) + + const state = createInitialRouterState({ + buildId, + initialTree, + initialHead: null, + initialCanonicalUrl, + initialSeedData: ['', {}, children], + initialParallelRoutes: new Map(), + isServer: false, + location: new URL('/linking/about', 'https://localhost') as any, + }) + + const action: ServerPatchAction = { + type: ACTION_SERVER_PATCH, + // this flight data is intentionally completely unrelated to the existing tree + flightData: [ + [ + 'children', + 'tree-patch-failure', + 'children', + 'new-page', + ['new-page', { children: ['__PAGE__', {}] }], + null, + null, + ], + ], + previousTree: [ + '', + { + children: [ + 'linking', + { + children: ['about', { children: ['', {}] }], + }, + ], + }, + undefined, + undefined, + true, + ], + overrideCanonicalUrl: undefined, + } + + const newState = await serverPatchReducer(state, action) + expect(newState.pushRef.pendingPush).toBe(true) + expect(newState.pushRef.mpaNavigation).toBe(true) + expect(newState.canonicalUrl).toBe('/linking/about') + }) }) diff --git a/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts index 08a5634a9b9ba..aafa98fcc2c46 100644 --- a/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts @@ -12,6 +12,7 @@ import { applyFlightData } from '../apply-flight-data' import { handleMutable } from '../handle-mutable' import type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime' import { createEmptyCacheNode } from '../../app-router' +import { handleSegmentMismatch } from '../handle-segment-mismatch' export function serverPatchReducer( state: ReadonlyReducerState, @@ -49,7 +50,7 @@ export function serverPatchReducer( ) if (newTree === null) { - throw new Error('SEGMENT MISMATCH') + return handleSegmentMismatch(state, action, treePatch) } if (isNavigatingToNewRootLayout(currentTree, newTree)) { From f758e3c208ccfb5a3faf3c99b99bbd7e87995d25 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Tue, 2 Jan 2024 19:27:38 -0800 Subject: [PATCH 2/2] Update packages/next/src/client/components/router-reducer/handle-segment-mismatch.ts Co-authored-by: JJ Kasper --- .../client/components/router-reducer/handle-segment-mismatch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/src/client/components/router-reducer/handle-segment-mismatch.ts b/packages/next/src/client/components/router-reducer/handle-segment-mismatch.ts index 0ef744f18f255..6ee410e7028ff 100644 --- a/packages/next/src/client/components/router-reducer/handle-segment-mismatch.ts +++ b/packages/next/src/client/components/router-reducer/handle-segment-mismatch.ts @@ -16,7 +16,7 @@ export function handleSegmentMismatch( ) { if (process.env.NODE_ENV === 'development') { console.warn( - 'Performing full reload because your application experienced an unrecoverable error. If this keeps occurring, please file a Next.js issue.\n\n' + + 'Performing hard navigation because your application experienced an unrecoverable error. If this keeps occurring, please file a Next.js issue.\n\n' + 'Reason: Segment mismatch\n' + `Last Action: ${action.type}\n\n` + `Current Tree: ${JSON.stringify(state.tree)}\n\n` +