Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
44f561c
fix(router-core,ssr-client): catch errors thrown from head
roduyemi Oct 9, 2025
b343aaa
chore(e2e): add test case for head not found to solid-start
roduyemi Oct 9, 2025
1c27c12
chore(router-core): remove recursive call when handling before load e…
roduyemi Oct 9, 2025
9729f07
fix(router-core): types in hydrate.test
roduyemi Oct 9, 2025
2b309c5
chore: add eol to hydrate.test
roduyemi Oct 9, 2025
5347508
feat(router-core): move executeHead to beforeLoad
roduyemi Oct 9, 2025
20b60af
chore: execute head functions
roduyemi Oct 27, 2025
1be11a3
Merge remote-tracking branch 'upstream/main' into head-404
roduyemi Oct 27, 2025
e53cf57
chore: only run excuteHead during SSR
roduyemi Oct 27, 2025
08c5f8e
fix: remove async on updateContext
roduyemi Oct 27, 2025
7cb7450
chore: execute head after loaders + add test to ensure correct error …
roduyemi Oct 28, 2025
254defe
chore: remove multiple loaders from via-head
roduyemi Oct 28, 2025
3abb562
chore: add via-loaders to test run
roduyemi Oct 28, 2025
5dd98c4
chore: add solid-start e2e via-loaders test
roduyemi Oct 28, 2025
fec4a1b
fix: load notFoundComponent with styles during before load error
roduyemi Oct 28, 2025
8238499
chore: remove matchId param from _handleNotFound
roduyemi Oct 28, 2025
366e4af
fix: check match.context before skipping head execution
roduyemi Oct 28, 2025
b12398d
chore: rename via-loaders to via-loader-with-context
roduyemi Oct 28, 2025
625c901
fix: update solid-router expected store updates
roduyemi Oct 28, 2025
d9b1951
chore: remove thenable
roduyemi Oct 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions e2e/react-start/basic/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/
import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
import { Route as NotFoundViaLoaderRouteImport } from './routes/not-found/via-loader'
import { Route as NotFoundViaHeadRouteImport } from './routes/not-found/via-head'
import { Route as NotFoundViaBeforeLoadRouteImport } from './routes/not-found/via-beforeLoad'
import { Route as ApiUsersRouteImport } from './routes/api.users'
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
Expand Down Expand Up @@ -169,6 +170,11 @@ const NotFoundViaLoaderRoute = NotFoundViaLoaderRouteImport.update({
path: '/via-loader',
getParentRoute: () => NotFoundRouteRoute,
} as any)
const NotFoundViaHeadRoute = NotFoundViaHeadRouteImport.update({
id: '/via-head',
path: '/via-head',
getParentRoute: () => NotFoundRouteRoute,
} as any)
const NotFoundViaBeforeLoadRoute = NotFoundViaBeforeLoadRouteImport.update({
id: '/via-beforeLoad',
path: '/via-beforeLoad',
Expand Down Expand Up @@ -272,6 +278,7 @@ export interface FileRoutesByFullPath {
'/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/api/users': typeof ApiUsersRouteWithChildren
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
'/not-found/via-head': typeof NotFoundViaHeadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
Expand Down Expand Up @@ -307,6 +314,7 @@ export interface FileRoutesByTo {
'/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/api/users': typeof ApiUsersRouteWithChildren
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
'/not-found/via-head': typeof NotFoundViaHeadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
'/posts/$postId': typeof PostsPostIdRoute
'/search-params/default': typeof SearchParamsDefaultRoute
Expand Down Expand Up @@ -347,6 +355,7 @@ export interface FileRoutesById {
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
'/not-found/via-head': typeof NotFoundViaHeadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
Expand Down Expand Up @@ -389,6 +398,7 @@ export interface FileRouteTypes {
| '/대한민국'
| '/api/users'
| '/not-found/via-beforeLoad'
| '/not-found/via-head'
| '/not-found/via-loader'
| '/posts/$postId'
| '/redirect/$target'
Expand Down Expand Up @@ -424,6 +434,7 @@ export interface FileRouteTypes {
| '/대한민국'
| '/api/users'
| '/not-found/via-beforeLoad'
| '/not-found/via-head'
| '/not-found/via-loader'
| '/posts/$postId'
| '/search-params/default'
Expand Down Expand Up @@ -463,6 +474,7 @@ export interface FileRouteTypes {
| '/_layout/_layout-2'
| '/api/users'
| '/not-found/via-beforeLoad'
| '/not-found/via-head'
| '/not-found/via-loader'
| '/posts/$postId'
| '/redirect/$target'
Expand Down Expand Up @@ -673,6 +685,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof NotFoundViaLoaderRouteImport
parentRoute: typeof NotFoundRouteRoute
}
'/not-found/via-head': {
id: '/not-found/via-head'
path: '/via-head'
fullPath: '/not-found/via-head'
preLoaderRoute: typeof NotFoundViaHeadRouteImport
parentRoute: typeof NotFoundRouteRoute
}
'/not-found/via-beforeLoad': {
id: '/not-found/via-beforeLoad'
path: '/via-beforeLoad'
Expand Down Expand Up @@ -797,12 +816,14 @@ declare module '@tanstack/react-router' {

interface NotFoundRouteRouteChildren {
NotFoundViaBeforeLoadRoute: typeof NotFoundViaBeforeLoadRoute
NotFoundViaHeadRoute: typeof NotFoundViaHeadRoute
NotFoundViaLoaderRoute: typeof NotFoundViaLoaderRoute
NotFoundIndexRoute: typeof NotFoundIndexRoute
}

const NotFoundRouteRouteChildren: NotFoundRouteRouteChildren = {
NotFoundViaBeforeLoadRoute: NotFoundViaBeforeLoadRoute,
NotFoundViaHeadRoute: NotFoundViaHeadRoute,
NotFoundViaLoaderRoute: NotFoundViaLoaderRoute,
NotFoundIndexRoute: NotFoundIndexRoute,
}
Expand Down
10 changes: 10 additions & 0 deletions e2e/react-start/basic/src/routes/not-found/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ export const Route = createFileRoute('/not-found/')({
via-loader
</Link>
</div>
<div className="mb-2">
<Link
from={Route.fullPath}
to="./via-head"
preload={preload}
data-testid="via-head"
>
via-head
</Link>
</div>
</div>
)
},
Expand Down
23 changes: 23 additions & 0 deletions e2e/react-start/basic/src/routes/not-found/via-head.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createFileRoute, notFound } from '@tanstack/react-router'

export const Route = createFileRoute('/not-found/via-head')({
head: () => {
throw notFound()
},
component: RouteComponent,
notFoundComponent: () => {
return (
<div data-testid="via-head-notFound-component">
Not Found "/not-found/via-head"!
</div>
)
},
})

function RouteComponent() {
return (
<div data-testid="via-head-route-component" data-server={typeof window}>
Hello "/not-found/via-head"!
</div>
)
}
8 changes: 3 additions & 5 deletions e2e/react-start/basic/tests/not-found.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const combinate = (combinateImport as any).default as typeof combinateImport
test.use({
whitelistErrors: [
/Failed to load resource: the server responded with a status of 404/,
'Error during route context hydration: {isNotFound: true}',
],
})
test.describe('not-found', () => {
Expand All @@ -24,8 +25,7 @@ test.describe('not-found', () => {

test.describe('throw notFound()', () => {
const navigationTestMatrix = combinate({
// TODO beforeLoad!
thrower: [/* 'beforeLoad',*/ 'loader'] as const,
thrower: ['beforeLoad', 'head', 'loader'] as const,
preload: [false, true] as const,
})

Expand Down Expand Up @@ -55,9 +55,7 @@ test.describe('not-found', () => {
})
})
const directVisitTestMatrix = combinate({
// TODO beforeLoad!

thrower: [/* 'beforeLoad',*/ 'loader'] as const,
thrower: ['beforeLoad', 'head', 'loader'] as const,
})

directVisitTestMatrix.forEach(({ thrower }) => {
Expand Down
21 changes: 21 additions & 0 deletions e2e/solid-start/basic/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/
import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
import { Route as NotFoundViaLoaderRouteImport } from './routes/not-found/via-loader'
import { Route as NotFoundViaHeadRouteImport } from './routes/not-found/via-head'
import { Route as NotFoundViaBeforeLoadRouteImport } from './routes/not-found/via-beforeLoad'
import { Route as ApiUsersRouteImport } from './routes/api/users'
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
Expand Down Expand Up @@ -156,6 +157,11 @@ const NotFoundViaLoaderRoute = NotFoundViaLoaderRouteImport.update({
path: '/via-loader',
getParentRoute: () => NotFoundRouteRoute,
} as any)
const NotFoundViaHeadRoute = NotFoundViaHeadRouteImport.update({
id: '/via-head',
path: '/via-head',
getParentRoute: () => NotFoundRouteRoute,
} as any)
const NotFoundViaBeforeLoadRoute = NotFoundViaBeforeLoadRouteImport.update({
id: '/via-beforeLoad',
path: '/via-beforeLoad',
Expand Down Expand Up @@ -244,6 +250,7 @@ export interface FileRoutesByFullPath {
'/users': typeof UsersRouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
'/not-found/via-head': typeof NotFoundViaHeadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
Expand Down Expand Up @@ -276,6 +283,7 @@ export interface FileRoutesByTo {
'/stream': typeof StreamRoute
'/api/users': typeof ApiUsersRouteWithChildren
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
'/not-found/via-head': typeof NotFoundViaHeadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
'/posts/$postId': typeof PostsPostIdRoute
'/search-params/default': typeof SearchParamsDefaultRoute
Expand Down Expand Up @@ -314,6 +322,7 @@ export interface FileRoutesById {
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
'/not-found/via-head': typeof NotFoundViaHeadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
Expand Down Expand Up @@ -352,6 +361,7 @@ export interface FileRouteTypes {
| '/users'
| '/api/users'
| '/not-found/via-beforeLoad'
| '/not-found/via-head'
| '/not-found/via-loader'
| '/posts/$postId'
| '/redirect/$target'
Expand Down Expand Up @@ -384,6 +394,7 @@ export interface FileRouteTypes {
| '/stream'
| '/api/users'
| '/not-found/via-beforeLoad'
| '/not-found/via-head'
| '/not-found/via-loader'
| '/posts/$postId'
| '/search-params/default'
Expand Down Expand Up @@ -421,6 +432,7 @@ export interface FileRouteTypes {
| '/_layout/_layout-2'
| '/api/users'
| '/not-found/via-beforeLoad'
| '/not-found/via-head'
| '/not-found/via-loader'
| '/posts/$postId'
| '/redirect/$target'
Expand Down Expand Up @@ -619,6 +631,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof NotFoundViaLoaderRouteImport
parentRoute: typeof NotFoundRouteRoute
}
'/not-found/via-head': {
id: '/not-found/via-head'
path: '/via-head'
fullPath: '/not-found/via-head'
preLoaderRoute: typeof NotFoundViaHeadRouteImport
parentRoute: typeof NotFoundRouteRoute
}
'/not-found/via-beforeLoad': {
id: '/not-found/via-beforeLoad'
path: '/via-beforeLoad'
Expand Down Expand Up @@ -722,12 +741,14 @@ declare module '@tanstack/solid-router' {

interface NotFoundRouteRouteChildren {
NotFoundViaBeforeLoadRoute: typeof NotFoundViaBeforeLoadRoute
NotFoundViaHeadRoute: typeof NotFoundViaHeadRoute
NotFoundViaLoaderRoute: typeof NotFoundViaLoaderRoute
NotFoundIndexRoute: typeof NotFoundIndexRoute
}

const NotFoundRouteRouteChildren: NotFoundRouteRouteChildren = {
NotFoundViaBeforeLoadRoute: NotFoundViaBeforeLoadRoute,
NotFoundViaHeadRoute: NotFoundViaHeadRoute,
NotFoundViaLoaderRoute: NotFoundViaLoaderRoute,
NotFoundIndexRoute: NotFoundIndexRoute,
}
Expand Down
10 changes: 10 additions & 0 deletions e2e/solid-start/basic/src/routes/not-found/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ export const Route = createFileRoute('/not-found/')({
via-loader
</Link>
</div>
<div class="mb-2">
<Link
from={Route.fullPath}
to="./via-head"
preload={preload()}
data-testid="via-head"
>
via-head
</Link>
</div>
</div>
)
},
Expand Down
23 changes: 23 additions & 0 deletions e2e/solid-start/basic/src/routes/not-found/via-head.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createFileRoute, notFound } from '@tanstack/solid-router'

export const Route = createFileRoute('/not-found/via-head')({
head: () => {
throw notFound()
},
component: RouteComponent,
notFoundComponent: () => {
return (
<div data-testid="via-head-notFound-component">
Not Found "/not-found/via-head"!
</div>
)
},
})

function RouteComponent() {
return (
<div data-testid="via-head-route-component" data-server={typeof window}>
Hello "/not-found/via-head"!
</div>
)
}
8 changes: 3 additions & 5 deletions e2e/solid-start/basic/tests/not-found.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const combinate = (combinateImport as any).default as typeof combinateImport
test.use({
whitelistErrors: [
/Failed to load resource: the server responded with a status of 404/,
'Error during route context hydration: {isNotFound: true}',
],
})
test.describe('not-found', () => {
Expand All @@ -24,8 +25,7 @@ test.describe('not-found', () => {

test.describe('throw notFound()', () => {
const navigationTestMatrix = combinate({
// TODO beforeLoad!
thrower: [/* 'beforeLoad',*/ 'loader'] as const,
thrower: ['beforeLoad', 'head', 'loader'] as const,
preload: [false, true] as const,
})

Expand Down Expand Up @@ -54,9 +54,7 @@ test.describe('not-found', () => {
})
})
const directVisitTestMatrix = combinate({
// TODO beforeLoad!

thrower: [/* 'beforeLoad',*/ 'loader'] as const,
thrower: ['beforeLoad', 'head', 'loader'] as const,
})

directVisitTestMatrix.forEach(({ thrower }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ describe("Store doesn't update *too many* times during navigation", () => {
// This number should be as small as possible to minimize the amount of work
// that needs to be done during a navigation.
// Any change that increases this number should be investigated.
expect(updates).toBe(7)
expect(updates).toBe(6)
})

test('hover preload, then navigate, w/ async loaders', async () => {
Expand Down
16 changes: 10 additions & 6 deletions packages/router-core/src/load-matches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,18 @@ const resolvePreload = (inner: InnerLoadContext, matchId: string): boolean => {
const _handleNotFound = (inner: InnerLoadContext, err: NotFoundError) => {
// Find the route that should handle the not found error
// First check if a specific route is requested to show the error
const routeCursor =
let routeCursor =
inner.router.routesById[err.routeId ?? ''] ?? inner.router.routeTree

// For BEFORE_LOAD errors, find a parent route with a notFoundComponent that can handle the error
if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
while (routeCursor.parentRoute && !routeCursor.options.notFoundComponent) {
routeCursor = routeCursor.parentRoute
}
// Update the error to point to the error handling route
err.routeId = routeCursor.id
}
Comment on lines 56 to 73
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify the NotFound ancestor routing behavior.

The new logic correctly climbs the route tree to find an ancestor with a notFoundComponent when handling BEFORE_LOAD errors. This is a significant change in error handling behavior.

Run the following script to verify this logic is exercised in tests:


🏁 Script executed:

#!/bin/bash
# Description: Find test coverage for BEFORE_LOAD NotFound handling

# Search for tests that exercise NotFound errors during beforeLoad
rg -nP -A5 -B5 'BEFORE_LOAD|beforeLoad.*notFound|notFound.*beforeLoad' --type=ts --type=tsx -g '**/*test*' -g '**/*spec*'

# Search for routes with notFoundComponent to understand the hierarchy
ast-grep --pattern $'notFoundComponent: $_'

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Retry search for test coverage of BEFORE_LOAD NotFound handling without type filters

# Search for tests mentioning BEFORE_LOAD or notFound in .ts/.js test files
rg -nP -C3 'BEFORE_LOAD|beforeLoad|notFound' -g '**/*.{test,spec}.{ts,js}'

# Search for definitions of notFoundComponent across codebase
rg -nP 'notFoundComponent' -g '**/*.{ts,js}'

Length of output: 64948


Add unit tests for BEFORE_LOAD notFound ancestor routing.
Current tests don’t cover the case where a nested route’s beforeLoad throws notFound() and the logic climbs to a parent route with a notFoundComponent; please add tests to assert that err.routeId is updated to the correct ancestor.


// Ensure a NotFoundComponent exists on the route
if (
!routeCursor.options.notFoundComponent &&
Expand Down Expand Up @@ -84,11 +93,6 @@ const _handleNotFound = (inner: InnerLoadContext, err: NotFoundError) => {
error: err,
isFetching: false,
}))

if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
err.routeId = routeCursor.parentRoute.id
_handleNotFound(inner, err)
}
}

const handleRedirectAndNotFound = (
Expand Down
4 changes: 3 additions & 1 deletion packages/router-core/src/ssr/ssr-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,9 @@ export async function hydrate(router: AnyRouter): Promise<any> {
match.styles = headFnContent?.styles
match.scripts = scripts
}),
)
).catch((err) => {
console.error('Error during route context hydration:', err)
})

const isSpaMode = matches[matches.length - 1]!.id !== lastMatchId
const hasSsrFalseMatches = matches.some((m) => m.ssr === false)
Expand Down
Loading