diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx index 90be8b8db29..1fd85918efd 100644 --- a/packages/react-router/tests/router.test.tsx +++ b/packages/react-router/tests/router.test.tsx @@ -8,7 +8,7 @@ import { waitFor, } from '@testing-library/react' import { z } from 'zod' -import { composeRewrites } from '@tanstack/router-core' +import { composeRewrites, notFound } from '@tanstack/router-core' import { Link, Outlet, @@ -1929,13 +1929,11 @@ describe('does not strip search params if search validation fails', () => { }) }) -describe('statusCode reset on navigation', () => { +describe('statusCode', () => { it('should reset statusCode to 200 when navigating from 404 to valid route', async () => { const history = createMemoryHistory({ initialEntries: ['/'] }) - const rootRoute = createRootRoute({ - component: () => , - }) + const rootRoute = createRootRoute() const indexRoute = createRoute({ getParentRoute: () => rootRoute, @@ -1968,6 +1966,174 @@ describe('statusCode reset on navigation', () => { await act(() => router.navigate({ to: '/another-non-existing' })) expect(router.state.statusCode).toBe(404) }) + + describe.each([true, false])( + 'status code is set when loader/beforeLoad throws (isAsync=%s)', + async (isAsync) => { + const throwingFun = isAsync + ? (toThrow: () => void) => async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + toThrow() + } + : (toThrow: () => void) => toThrow + + const throwNotFound = throwingFun(() => { + throw notFound() + }) + const throwError = throwingFun(() => { + throw new Error('test-error') + }) + it('should set statusCode to 404 when a route loader throws a notFound()', async () => { + const history = createMemoryHistory({ initialEntries: ['/'] }) + + const rootRoute = createRootRoute() + + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () =>
Home
, + }) + + const loaderThrowsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/loader-throws-not-found', + loader: throwNotFound, + component: () => ( +
loader will throw
+ ), + notFoundComponent: () => ( +
Not Found
+ ), + }) + + const routeTree = rootRoute.addChildren([indexRoute, loaderThrowsRoute]) + const router = createRouter({ routeTree, history }) + + render() + + expect(router.state.statusCode).toBe(200) + + await act(() => router.navigate({ to: '/loader-throws-not-found' })) + expect(router.state.statusCode).toBe(404) + expect( + await screen.findByTestId('not-found-component'), + ).toBeInTheDocument() + expect(screen.queryByTestId('route-component')).not.toBeInTheDocument() + }) + + it('should set statusCode to 404 when a route beforeLoad throws a notFound()', async () => { + const history = createMemoryHistory({ initialEntries: ['/'] }) + + const rootRoute = createRootRoute() + + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () =>
Home
, + }) + + const beforeLoadThrowsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/beforeload-throws-not-found', + beforeLoad: throwNotFound, + component: () => ( +
beforeLoad will throw
+ ), + notFoundComponent: () => ( +
Not Found
+ ), + }) + + const routeTree = rootRoute.addChildren([ + indexRoute, + beforeLoadThrowsRoute, + ]) + const router = createRouter({ routeTree, history }) + + render() + + expect(router.state.statusCode).toBe(200) + + await act(() => router.navigate({ to: '/beforeload-throws-not-found' })) + expect(router.state.statusCode).toBe(404) + expect( + await screen.findByTestId('not-found-component'), + ).toBeInTheDocument() + expect(screen.queryByTestId('route-component')).not.toBeInTheDocument() + }) + + it('should set statusCode to 500 when a route loader throws an Error', async () => { + const history = createMemoryHistory({ initialEntries: ['/'] }) + + const rootRoute = createRootRoute() + + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () =>
Home
, + }) + + const loaderThrowsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/loader-throws-error', + loader: throwError, + component: () => ( +
loader will throw
+ ), + errorComponent: () =>
Error
, + }) + + const routeTree = rootRoute.addChildren([indexRoute, loaderThrowsRoute]) + const router = createRouter({ routeTree, history }) + + render() + + expect(router.state.statusCode).toBe(200) + + await act(() => router.navigate({ to: '/loader-throws-error' })) + expect(router.state.statusCode).toBe(500) + expect(await screen.findByTestId('error-component')).toBeInTheDocument() + expect(screen.queryByTestId('route-component')).not.toBeInTheDocument() + }) + + it('should set statusCode to 500 when a route beforeLoad throws an Error', async () => { + const history = createMemoryHistory({ initialEntries: ['/'] }) + + const rootRoute = createRootRoute() + + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () =>
Home
, + }) + + const beforeLoadThrowsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/beforeload-throws-error', + beforeLoad: throwError, + component: () => ( +
beforeLoad will throw
+ ), + errorComponent: () =>
Error
, + }) + + const routeTree = rootRoute.addChildren([ + indexRoute, + beforeLoadThrowsRoute, + ]) + const router = createRouter({ routeTree, history }) + + render() + + expect(router.state.statusCode).toBe(200) + + await act(() => router.navigate({ to: '/beforeload-throws-error' })) + expect(router.state.statusCode).toBe(500) + expect(await screen.findByTestId('error-component')).toBeInTheDocument() + expect(screen.queryByTestId('route-component')).not.toBeInTheDocument() + }) + }, + ) }) describe('Router rewrite functionality', () => { diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index eb9f67113b6..e4f35570e40 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -2135,10 +2135,16 @@ export class RouterCore< await this.latestLoadPromise } + let newStatusCode: number | undefined = undefined if (this.hasNotFoundMatch()) { + newStatusCode = 404 + } else if (this.__store.state.matches.some((d) => d.status === 'error')) { + newStatusCode = 500 + } + if (newStatusCode !== undefined) { this.__store.setState((s) => ({ ...s, - statusCode: 404, + statusCode: newStatusCode, })) } }