Skip to content

Conversation

@joseph0926
Copy link

@joseph0926 joseph0926 commented Nov 14, 2025

Summary

Fixes #5733

Previously, these errors were wrapped in PathParamError, causing a 500 error instead of 404.
When notFound() is thrown from params.parse, the router now correctly Sets match status to notFound

Changes

  • Changed paramsError type from PathParamError | undefined to unknown
  • NotFoundError and Redirect directly in paramsError
  • Added tests in load.test.ts for params.parse throwing notFound()

Test Plan

  • params.parse throwing notFound() sets match status to 'notFound' and statusCode to 404
  • params.parse with valid input succeeds with status 'success' and statusCode 200

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced error handling during route parameter parsing to properly distinguish between NotFound, Redirect, and other error types, improving error propagation behavior when route matching fails.
  • Tests

    • Added test coverage for route parameter parsing scenarios, validating correct behavior for both invalid and valid route identifiers in parameter validation.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 14, 2025

Walkthrough

Modifies path-parameter error handling in RouterCore to propagate NotFound and Redirect errors directly from params.parse, while wrapping other errors in PathParamError. Updates paramsError type from PathParamError | undefined to unknown. Adds test coverage for notFound behavior during param parsing.

Changes

Cohort / File(s) Summary
Router error handling
packages/router-core/src/router.ts
Modified strict route parameter parsing to distinguish error types: NotFound and Redirect errors from params.parse are assigned directly to paramsError; other errors are wrapped in PathParamError. Changed paramsError type from PathParamError | undefined to unknown.
Param parsing tests
packages/router-core/tests/load.test.ts
Added test coverage for params.parse notFound behavior with custom parse functions for route parameters. Includes two describe blocks (duplicated structure) each validating notFound rejection for invalid params and success for valid params.

Sequence Diagram

sequenceDiagram
    participant Router
    participant ParamParser as Param Parser
    participant ErrorHandler as Error Handler
    
    rect rgb(200, 220, 255)
    Note over Router,ErrorHandler: Old Behavior (All errors wrapped)
    Router->>ParamParser: parse(params)
    ParamParser-->>Router: Error (any type)
    Router->>ErrorHandler: Wrap in PathParamError
    ErrorHandler-->>Router: PathParamError
    end
    
    rect rgb(220, 255, 220)
    Note over Router,ErrorHandler: New Behavior (Type-aware propagation)
    Router->>ParamParser: parse(params)
    alt NotFound or Redirect
        ParamParser-->>Router: NotFound/Redirect
        Router->>ErrorHandler: Propagate as-is
    else Other Error
        ParamParser-->>Router: Other Error
        Router->>ErrorHandler: Wrap in PathParamError
    end
    ErrorHandler-->>Router: NotFound/Redirect or PathParamError
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Type signature change to paramsError: unknown requires verification of all code paths consuming this variable
  • Error propagation logic change: verify NotFound/Redirect distinction is correctly applied across all param-parsing scenarios
  • Note: Test file contains duplicated describe blocks with identical test structures—confirm if intentional or requires consolidation

Possibly related PRs

Suggested labels

package: router-core

Suggested reviewers

  • schiller-manuel
  • Sheraff

Poem

🐰 A rabbit hops through routing code so fine,
Where 404s now properly align!
NotFound errors skip their wrapping vest,
Propagated true to pass the test. ✨
No more mysterious 500s in sight,
Just honest 404s shining bright!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: handle notFound from params.parse correctly' directly describes the main change: fixing the handling of notFound errors thrown from params.parse functions.
Linked Issues check ✅ Passed The PR successfully addresses issue #5733 by changing error handling so notFound errors from params.parse are no longer wrapped in PathParamError, resulting in correct 404 responses instead of 500 errors.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to fixing params.parse notFound handling: router error handling logic and corresponding test coverage.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/router-core/tests/load.test.ts (2)

512-570: Consider extracting shared test setup to reduce duplication.

Both tests have nearly identical setup code (route creation, router instantiation). This could be refactored into a shared helper function or beforeEach block to improve maintainability.

Example refactor:

describe('params.parse notFound', () => {
  const setupRouter = (initialPath: string) => {
    const rootRoute = new BaseRootRoute({})
    const testRoute = new BaseRoute({
      getParentRoute: () => rootRoute,
      path: '/test/$id',
      params: {
        parse: ({ id }: { id: string }) => {
          const parsed = parseInt(id, 10)
          if (Number.isNaN(parsed)) {
            throw notFound()
          }
          return { id: parsed }
        },
      },
    })
    const routeTree = rootRoute.addChildren([testRoute])
    const router = new RouterCore({
      routeTree,
      history: createMemoryHistory({ initialEntries: [initialPath] }),
    })
    return { router, testRoute }
  }

  test('throws notFound on invalid params', async () => {
    const { router, testRoute } = setupRouter('/test/invalid')
    await router.load()
    const match = router.state.pendingMatches?.find(
      (m) => m.routeId === testRoute.id,
    )
    expect(match?.status).toBe('notFound')
  })

  test('succeeds on valid params', async () => {
    const { router, testRoute } = setupRouter('/test/123')
    await router.load()
    const match = router.state.matches.find((m) => m.routeId === testRoute.id)
    expect(match?.status).toBe('success')
    expect(router.state.statusCode).toBe(200)
  })
})

512-570: Consider adding test coverage for redirect() in params.parse.

The PR changes handle redirect() thrown from params.parse as a special case (similar to notFound()), but there's no test coverage for this scenario. Adding a test would ensure redirect behavior works correctly.

Example test:

test('throws redirect on conditional params', async () => {
  const rootRoute = new BaseRootRoute({})
  const testRoute = new BaseRoute({
    getParentRoute: () => rootRoute,
    path: '/test/$id',
    params: {
      parse: ({ id }: { id: string }) => {
        if (id === 'redirect-me') {
          throw redirect({ to: '/other' })
        }
        return { id }
      },
    },
  })
  const routeTree = rootRoute.addChildren([testRoute])
  const router = new RouterCore({
    routeTree,
    history: createMemoryHistory({ initialEntries: ['/test/redirect-me'] }),
  })

  await router.load()

  expect(router.state.redirect).toBeDefined()
})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c05679e and 4e58a07.

📒 Files selected for processing (2)
  • packages/router-core/src/router.ts (2 hunks)
  • packages/router-core/tests/load.test.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: FatahChan
Repo: TanStack/router PR: 5475
File: e2e/react-start/basic-prerendering/src/routes/redirect/$target/via-beforeLoad.tsx:8-0
Timestamp: 2025-10-14T18:59:33.990Z
Learning: In TanStack Router e2e test files, when a route parameter is validated at the route level (e.g., using zod in validateSearch or param validation), switch statements on that parameter do not require a default case, as the validation ensures only expected values will reach the switch.
📚 Learning: 2025-10-14T18:59:33.990Z
Learnt from: FatahChan
Repo: TanStack/router PR: 5475
File: e2e/react-start/basic-prerendering/src/routes/redirect/$target/via-beforeLoad.tsx:8-0
Timestamp: 2025-10-14T18:59:33.990Z
Learning: In TanStack Router e2e test files, when a route parameter is validated at the route level (e.g., using zod in validateSearch or param validation), switch statements on that parameter do not require a default case, as the validation ensures only expected values will reach the switch.

Applied to files:

  • packages/router-core/tests/load.test.ts
  • packages/router-core/src/router.ts
📚 Learning: 2025-10-08T08:11:47.088Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.

Applied to files:

  • packages/router-core/tests/load.test.ts
📚 Learning: 2025-10-01T18:30:26.591Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.

Applied to files:

  • packages/router-core/src/router.ts
🧬 Code graph analysis (1)
packages/router-core/src/router.ts (2)
packages/router-core/src/index.ts (3)
  • isNotFound (380-380)
  • isRedirect (374-374)
  • PathParamError (204-204)
packages/router-core/src/redirect.ts (1)
  • isRedirect (118-120)
🔇 Additional comments (2)
packages/router-core/src/router.ts (2)

1382-1382: Type change enables direct propagation of NotFound and Redirect errors.

The type change from PathParamError | undefined to unknown is correct. This allows paramsError to hold NotFoundError, Redirect, or PathParamError objects, enabling the router to distinguish between navigation control flow errors (notFound/redirect) and actual parameter validation errors.


1395-1401: Error handling correctly differentiates NotFound/Redirect from validation errors.

The updated logic properly handles the three error cases:

  1. NotFound and Redirect errors are assigned directly to paramsError, allowing the router to recognize them as navigation control flow
  2. Other errors are wrapped in PathParamError to indicate parameter validation failures

This resolves issue #5733 where notFound() thrown from params.parse was incorrectly wrapped in PathParamError, resulting in a 500 response instead of 404.

Comment on lines +513 to +541
test('throws notFound on invalid params', async () => {
const rootRoute = new BaseRootRoute({})
const testRoute = new BaseRoute({
getParentRoute: () => rootRoute,
path: '/test/$id',
params: {
parse: ({ id }: { id: string }) => {
const parsed = parseInt(id, 10)
if (Number.isNaN(parsed)) {
throw notFound()
}
return { id: parsed }
},
},
})
const routeTree = rootRoute.addChildren([testRoute])
const router = new RouterCore({
routeTree,
history: createMemoryHistory({ initialEntries: ['/test/invalid'] }),
})

await router.load()

const match = router.state.pendingMatches?.find(
(m) => m.routeId === testRoute.id,
)

expect(match?.status).toBe('notFound')
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add statusCode assertion to verify 404 response.

The test verifies the match status is 'notFound' but doesn't check that the router's statusCode is set to 404. According to the PR objectives, this test should verify both the match status and the statusCode.

Add this assertion after line 540:

 expect(match?.status).toBe('notFound')
+expect(router.state.statusCode).toBe(404)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test('throws notFound on invalid params', async () => {
const rootRoute = new BaseRootRoute({})
const testRoute = new BaseRoute({
getParentRoute: () => rootRoute,
path: '/test/$id',
params: {
parse: ({ id }: { id: string }) => {
const parsed = parseInt(id, 10)
if (Number.isNaN(parsed)) {
throw notFound()
}
return { id: parsed }
},
},
})
const routeTree = rootRoute.addChildren([testRoute])
const router = new RouterCore({
routeTree,
history: createMemoryHistory({ initialEntries: ['/test/invalid'] }),
})
await router.load()
const match = router.state.pendingMatches?.find(
(m) => m.routeId === testRoute.id,
)
expect(match?.status).toBe('notFound')
})
test('throws notFound on invalid params', async () => {
const rootRoute = new BaseRootRoute({})
const testRoute = new BaseRoute({
getParentRoute: () => rootRoute,
path: '/test/$id',
params: {
parse: ({ id }: { id: string }) => {
const parsed = parseInt(id, 10)
if (Number.isNaN(parsed)) {
throw notFound()
}
return { id: parsed }
},
},
})
const routeTree = rootRoute.addChildren([testRoute])
const router = new RouterCore({
routeTree,
history: createMemoryHistory({ initialEntries: ['/test/invalid'] }),
})
await router.load()
const match = router.state.pendingMatches?.find(
(m) => m.routeId === testRoute.id,
)
expect(match?.status).toBe('notFound')
expect(router.state.statusCode).toBe(404)
})
🤖 Prompt for AI Agents
In packages/router-core/tests/load.test.ts around lines 513 to 541, the test
checks that the match status is 'notFound' but doesn't assert the router's
overall statusCode; add an assertion after the existing
expect(match?.status).toBe('notFound') to also expect router.statusCode to equal
404 so the test verifies both the match status and the router's 404 statusCode.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

notFound in parameter parser returns 500, not 404

1 participant