Skip to content

Commit

Permalink
gracefully handle client router segment mismatches (#60141)
Browse files Browse the repository at this point in the history
### What?
Next.js throws a hard `SEGMENT MISMATCH` error when the reducers were
unable to apply the a patch to the router tree from the server response.

### How?
Rather than crashing the router, this will treat segment mismatches as a
MPA navigation, to restore the client router into a working state.

### Test Plan
If there are specific scenarios where Next.js throws this error, it
should most likely be fixed in Next and not in user-land. Since it's not
currently obvious what scenarios will trigger this error, this PR serves
to recover from a mismatch more gracefully and provides some debug
information rather than crashing the application. As such, there's no
easy way to create an E2E test for this and I've instead opted for a
simple unit test.

Closes NEXT-1878
[slack
x-ref](https://vercel.slack.com/archives/C017QMYC5FB/p1704214439768469)
[slack
x-ref](https://vercel.slack.com/archives/C03KAR5DCKC/p1702565978694519)

---------

Co-authored-by: JJ Kasper <jj@jjsweb.site>
  • Loading branch information
ztanner and ijjk authored Jan 3, 2024
1 parent 5d5f585 commit d52d36d
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -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 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` +
`Tree Patch Payload: ${JSON.stringify(treePatch)}`
)
}

return handleExternalUrl(state, {}, state.canonicalUrl, true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -71,7 +72,7 @@ function fastRefreshReducerImpl(
)

if (newTree === null) {
throw new Error('SEGMENT MISMATCH')
return handleSegmentMismatch(state, action, treePatch)
}

if (isNavigatingToNewRootLayout(currentTree, newTree)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -69,7 +70,7 @@ export function refreshReducer(
)

if (newTree === null) {
throw new Error('SEGMENT MISMATCH')
return handleSegmentMismatch(state, action, treePatch)
}

if (isNavigatingToNewRootLayout(currentTree, newTree)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -224,7 +225,7 @@ export function serverActionReducer(
)

if (newTree === null) {
throw new Error('SEGMENT MISMATCH')
return handleSegmentMismatch(state, action, treePatch)
}

if (isNavigatingToNewRootLayout(currentTree, newTree)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
<html>
<head></head>
<body>Root layout</body>
</html>
)

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')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -49,7 +50,7 @@ export function serverPatchReducer(
)

if (newTree === null) {
throw new Error('SEGMENT MISMATCH')
return handleSegmentMismatch(state, action, treePatch)
}

if (isNavigatingToNewRootLayout(currentTree, newTree)) {
Expand Down

0 comments on commit d52d36d

Please sign in to comment.