Skip to content

Commit 2f53749

Browse files
fix: fix primitive beforeLoad errors (#7505)
* fix: fix primitive beforeLoad errors * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 86b8f9f commit 2f53749

8 files changed

Lines changed: 199 additions & 7 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@tanstack/router-core': patch
3+
'@tanstack/react-router': patch
4+
'@tanstack/react-start': patch
5+
---
6+
7+
Preserve primitive values thrown from beforeLoad error handling.

e2e/react-start/basic/src/routeTree.gen.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Route as TypeOnlyReexportRouteImport } from './routes/type-only-reexpor
1414
import { Route as StreamRouteImport } from './routes/stream'
1515
import { Route as ScriptsRouteImport } from './routes/scripts'
1616
import { Route as RawStreamRouteImport } from './routes/raw-stream'
17+
import { Route as PrimitiveBeforeloadErrorRouteImport } from './routes/primitive-beforeload-error'
1718
import { Route as PostsRouteImport } from './routes/posts'
1819
import { Route as PlainTsTypeAssertionRouteImport } from './routes/plain-ts-type-assertion'
1920
import { Route as LinksRouteImport } from './routes/links'
@@ -104,6 +105,12 @@ const RawStreamRoute = RawStreamRouteImport.update({
104105
path: '/raw-stream',
105106
getParentRoute: () => rootRouteImport,
106107
} as any)
108+
const PrimitiveBeforeloadErrorRoute =
109+
PrimitiveBeforeloadErrorRouteImport.update({
110+
id: '/primitive-beforeload-error',
111+
path: '/primitive-beforeload-error',
112+
getParentRoute: () => rootRouteImport,
113+
} as any)
107114
const PostsRoute = PostsRouteImport.update({
108115
id: '/posts',
109116
path: '/posts',
@@ -451,6 +458,7 @@ export interface FileRoutesByFullPath {
451458
'/links': typeof LinksRoute
452459
'/plain-ts-type-assertion': typeof PlainTsTypeAssertionRoute
453460
'/posts': typeof PostsRouteWithChildren
461+
'/primitive-beforeload-error': typeof PrimitiveBeforeloadErrorRoute
454462
'/raw-stream': typeof RawStreamRouteWithChildren
455463
'/scripts': typeof ScriptsRoute
456464
'/stream': typeof StreamRoute
@@ -517,6 +525,7 @@ export interface FileRoutesByTo {
517525
'/inline-scripts': typeof InlineScriptsRoute
518526
'/links': typeof LinksRoute
519527
'/plain-ts-type-assertion': typeof PlainTsTypeAssertionRoute
528+
'/primitive-beforeload-error': typeof PrimitiveBeforeloadErrorRoute
520529
'/scripts': typeof ScriptsRoute
521530
'/stream': typeof StreamRoute
522531
'/type-only-reexport': typeof TypeOnlyReexportRoute
@@ -582,6 +591,7 @@ export interface FileRoutesById {
582591
'/links': typeof LinksRoute
583592
'/plain-ts-type-assertion': typeof PlainTsTypeAssertionRoute
584593
'/posts': typeof PostsRouteWithChildren
594+
'/primitive-beforeload-error': typeof PrimitiveBeforeloadErrorRoute
585595
'/raw-stream': typeof RawStreamRouteWithChildren
586596
'/scripts': typeof ScriptsRoute
587597
'/stream': typeof StreamRoute
@@ -654,6 +664,7 @@ export interface FileRouteTypes {
654664
| '/links'
655665
| '/plain-ts-type-assertion'
656666
| '/posts'
667+
| '/primitive-beforeload-error'
657668
| '/raw-stream'
658669
| '/scripts'
659670
| '/stream'
@@ -720,6 +731,7 @@ export interface FileRouteTypes {
720731
| '/inline-scripts'
721732
| '/links'
722733
| '/plain-ts-type-assertion'
734+
| '/primitive-beforeload-error'
723735
| '/scripts'
724736
| '/stream'
725737
| '/type-only-reexport'
@@ -784,6 +796,7 @@ export interface FileRouteTypes {
784796
| '/links'
785797
| '/plain-ts-type-assertion'
786798
| '/posts'
799+
| '/primitive-beforeload-error'
787800
| '/raw-stream'
788801
| '/scripts'
789802
| '/stream'
@@ -856,6 +869,7 @@ export interface RootRouteChildren {
856869
LinksRoute: typeof LinksRoute
857870
PlainTsTypeAssertionRoute: typeof PlainTsTypeAssertionRoute
858871
PostsRoute: typeof PostsRouteWithChildren
872+
PrimitiveBeforeloadErrorRoute: typeof PrimitiveBeforeloadErrorRoute
859873
RawStreamRoute: typeof RawStreamRouteWithChildren
860874
ScriptsRoute: typeof ScriptsRoute
861875
StreamRoute: typeof StreamRoute
@@ -907,6 +921,13 @@ declare module '@tanstack/react-router' {
907921
preLoaderRoute: typeof RawStreamRouteImport
908922
parentRoute: typeof rootRouteImport
909923
}
924+
'/primitive-beforeload-error': {
925+
id: '/primitive-beforeload-error'
926+
path: '/primitive-beforeload-error'
927+
fullPath: '/primitive-beforeload-error'
928+
preLoaderRoute: typeof PrimitiveBeforeloadErrorRouteImport
929+
parentRoute: typeof rootRouteImport
930+
}
910931
'/posts': {
911932
id: '/posts'
912933
path: '/posts'
@@ -1620,6 +1641,7 @@ const rootRouteChildren: RootRouteChildren = {
16201641
LinksRoute: LinksRoute,
16211642
PlainTsTypeAssertionRoute: PlainTsTypeAssertionRoute,
16221643
PostsRoute: PostsRouteWithChildren,
1644+
PrimitiveBeforeloadErrorRoute: PrimitiveBeforeloadErrorRoute,
16231645
RawStreamRoute: RawStreamRouteWithChildren,
16241646
ScriptsRoute: ScriptsRoute,
16251647
StreamRoute: StreamRoute,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
3+
export const Route = createFileRoute('/primitive-beforeload-error')({
4+
beforeLoad: () => {
5+
throw 'primitive beforeLoad error'
6+
},
7+
component: () => {
8+
return (
9+
<div data-testid="primitive-beforeload-route-component">
10+
This should not render.
11+
</div>
12+
)
13+
},
14+
errorComponent: ({ error }) => {
15+
return (
16+
<div data-testid="primitive-beforeload-error-component">
17+
{String(error)}
18+
</div>
19+
)
20+
},
21+
})

e2e/react-start/basic/start-mode-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export function getStartModeConfig() {
3232
'/redirect',
3333
'/i-do-not-exist',
3434
'/not-found',
35+
'/primitive-beforeload-error',
3536
'/specialChars/search',
3637
'/specialChars/hash',
3738
'/specialChars/malformed',
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { expect } from '@playwright/test'
2+
import { test } from '@tanstack/router-e2e-utils'
3+
import { isSpaMode } from './utils/isSpaMode'
4+
5+
test.use({
6+
whitelistErrors: [
7+
'Failed to load resource: the server responded with a status of 500',
8+
'primitive beforeLoad error',
9+
],
10+
})
11+
12+
test('beforeLoad primitive throw renders error component on direct visit', async ({
13+
page,
14+
}) => {
15+
const response = await page.goto('/primitive-beforeload-error')
16+
await page.waitForLoadState('networkidle')
17+
18+
expect(response?.status()).toBe(isSpaMode ? 200 : 500)
19+
await expect(
20+
page.getByTestId('primitive-beforeload-error-component'),
21+
).toHaveText('primitive beforeLoad error')
22+
await expect(
23+
page.getByTestId('primitive-beforeload-route-component'),
24+
).not.toBeInViewport()
25+
})

packages/react-router/tests/errorComponent.test.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
22
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
3+
import ReactDOMServer from 'react-dom/server'
34

45
import {
56
Link,
@@ -28,6 +29,10 @@ function throwFn() {
2829
throw new Error('error thrown')
2930
}
3031

32+
function primitiveThrowFn() {
33+
throw 'primitive error thrown'
34+
}
35+
3136
let history: RouterHistory
3237

3338
beforeEach(() => {
@@ -262,6 +267,83 @@ describe.each([true, false])(
262267
},
263268
)
264269

270+
test('errorComponent receives primitive errors thrown from beforeLoad', async () => {
271+
const rootRoute = createRootRoute()
272+
const indexRoute = createRoute({
273+
getParentRoute: () => rootRoute,
274+
path: '/',
275+
component: function Home() {
276+
return (
277+
<div>
278+
<Link to="/about">link to about</Link>
279+
</div>
280+
)
281+
},
282+
})
283+
const aboutRoute = createRoute({
284+
getParentRoute: () => rootRoute,
285+
path: '/about',
286+
beforeLoad: primitiveThrowFn,
287+
component: function About() {
288+
return <div>About route content</div>
289+
},
290+
errorComponent: ({ error }) => <div>Error: {String(error)}</div>,
291+
})
292+
293+
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])
294+
295+
const router = createRouter({
296+
routeTree,
297+
history,
298+
})
299+
300+
render(<RouterProvider router={router} />)
301+
302+
const linkToAbout = await screen.findByRole('link', {
303+
name: 'link to about',
304+
})
305+
306+
await act(() => fireEvent.click(linkToAbout))
307+
308+
expect(
309+
await screen.findByText('Error: primitive error thrown', undefined, {
310+
timeout: 750,
311+
}),
312+
).toBeInTheDocument()
313+
expect(screen.queryByText('About route content')).not.toBeInTheDocument()
314+
})
315+
316+
test('SSR errorComponent receives primitive errors thrown from beforeLoad', async () => {
317+
const history = createMemoryHistory({ initialEntries: ['/about'] })
318+
const rootRoute = createRootRoute({
319+
component: function Root() {
320+
return <Outlet />
321+
},
322+
})
323+
const aboutRoute = createRoute({
324+
getParentRoute: () => rootRoute,
325+
path: '/about',
326+
beforeLoad: primitiveThrowFn,
327+
component: function About() {
328+
return <div>About route content</div>
329+
},
330+
errorComponent: ({ error }) => <div>Error: {String(error)}</div>,
331+
})
332+
333+
const router = createRouter({
334+
routeTree: rootRoute.addChildren([aboutRoute]),
335+
history,
336+
})
337+
router.isServer = true
338+
339+
await router.load()
340+
341+
expect(router.state.statusCode).toBe(500)
342+
const html = ReactDOMServer.renderToString(<RouterProvider router={router} />)
343+
expect(html).toContain('Error:')
344+
expect(html).toContain('primitive error thrown')
345+
})
346+
265347
describe('notFoundComponent is rendered when an error is thrown in params.parse', () => {
266348
test('displays notFoundComponent when error is thrown in params.parse', async () => {
267349
const history = createMemoryHistory({ initialEntries: ['/'] })

packages/router-core/src/load-matches.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@ const handleSerialError = (
207207
inner: InnerLoadContext,
208208
index: number,
209209
err: any,
210-
routerCode: string,
211210
): void => {
212211
const { id: matchId, routeId } = inner.matches[index]!
213212
const route = inner.router.looseRoutesById[routeId]!
@@ -219,7 +218,6 @@ const handleSerialError = (
219218
throw err
220219
}
221220

222-
err.routerCode = routerCode
223221
inner.firstBadMatchIndex ??= index
224222
handleRedirectAndNotFound(inner, inner.router.getMatch(matchId), err)
225223

@@ -405,11 +403,11 @@ const executeBeforeLoad = (
405403
const { paramsError, searchError } = match
406404

407405
if (paramsError) {
408-
handleSerialError(inner, index, paramsError, 'PARSE_PARAMS')
406+
handleSerialError(inner, index, paramsError)
409407
}
410408

411409
if (searchError) {
412-
handleSerialError(inner, index, searchError, 'VALIDATE_SEARCH')
410+
handleSerialError(inner, index, searchError)
413411
}
414412

415413
setupPendingTimeout(inner, matchId, route, match)
@@ -499,7 +497,7 @@ const executeBeforeLoad = (
499497
}
500498
if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
501499
pending()
502-
handleSerialError(inner, index, beforeLoadContext, 'BEFORE_LOAD')
500+
handleSerialError(inner, index, beforeLoadContext)
503501
}
504502

505503
inner.router.batch(() => {
@@ -519,13 +517,13 @@ const executeBeforeLoad = (
519517
pending()
520518
return beforeLoadContext
521519
.catch((err) => {
522-
handleSerialError(inner, index, err, 'BEFORE_LOAD')
520+
handleSerialError(inner, index, err)
523521
})
524522
.then(updateContext)
525523
}
526524
} catch (err) {
527525
pending()
528-
handleSerialError(inner, index, err, 'BEFORE_LOAD')
526+
handleSerialError(inner, index, err)
529527
}
530528

531529
updateContext(beforeLoadContext)

packages/router-core/tests/load.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,42 @@ describe('beforeLoad skip or exec', () => {
173173
expect(beforeLoad).toHaveBeenCalledTimes(1)
174174
})
175175

176+
test('preserves primitive errors thrown from beforeLoad', async () => {
177+
const beforeLoad = vi.fn<BeforeLoad>(() => {
178+
throw 'primitive error'
179+
})
180+
const router = setup({ beforeLoad })
181+
182+
await router.navigate({ to: '/foo' })
183+
184+
expect(router.state.statusCode).toBe(500)
185+
expect(router.state.matches).toEqual(
186+
expect.arrayContaining([
187+
expect.objectContaining({
188+
id: '/foo/foo',
189+
status: 'error',
190+
error: 'primitive error',
191+
}),
192+
]),
193+
)
194+
})
195+
196+
test('does not mutate object errors thrown from beforeLoad', async () => {
197+
const thrown = { type: 'domain-error' }
198+
const beforeLoad = vi.fn<BeforeLoad>(() => {
199+
throw thrown
200+
})
201+
const router = setup({ beforeLoad })
202+
203+
await router.navigate({ to: '/foo' })
204+
205+
expect(router.state.statusCode).toBe(500)
206+
expect(router.state.matches.find((d) => d.id === '/foo/foo')?.error).toBe(
207+
thrown,
208+
)
209+
expect(thrown).toEqual({ type: 'domain-error' })
210+
})
211+
176212
test('exec if resolved preload (success)', async () => {
177213
const beforeLoad = vi.fn()
178214
const router = setup({ beforeLoad })

0 commit comments

Comments
 (0)