Skip to content

Conversation

thenglong
Copy link

@thenglong thenglong commented Sep 26, 2025

Fixes: #5221

Summary by CodeRabbit

  • Bug Fixes

    • Improved base path handling in routing: URLs that end exactly at the base path or include various leading/trailing slash forms are now recognized and rewritten consistently, preventing navigation failures and incorrect routing across different deployments.
  • Tests

    • Added extensive tests covering many basepath and navigation scenarios to ensure consistent behavior.

Copy link
Contributor

coderabbitai bot commented Sep 26, 2025

Walkthrough

Replaced regex-based basepath rewriting with explicit normalization and matching logic in the router core; added extensive tests exercising basepath input/output rewriting across many scenarios. No public exports or signatures changed.

Changes

Cohort / File(s) Summary of changes
Router rewrite logic
packages/router-core/src/rewrite.ts
Rewrote rewriteBasepath internals: normalize basepath (ensure leading slash, compute trailing-slash variant), compute lowercase variants for case-insensitive handling, detect exact basepath and prefix matches, strip basepath portion or rewrite to /. Output joinPaths call remains unchanged; no export signature changes.
Tests for basepath handling
packages/react-router/tests/router.test.tsx
Added a comprehensive suite of tests for rewriteBasepath covering leading/trailing slash permutations, empty basepath, navigation (Link & programmatic), search/hash preservation, nested/compound rewrites, hostname/subdomain scenarios, and interactions with output rewrites.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant R as Router
  participant RB as rewriteBasepath

  C->>R: Request / Navigation with original URL
  R->>RB: rewriteBasepath(inputPath, basepath, options)
  alt basepath is empty or no match
    RB-->>R: return inputPath (unchanged)
  else exact basepath match (e.g., "/app")
    RB-->>R: return "/"
  else basepath prefix match (e.g., "/app/other")
    RB-->>R: return path with basepath stripped ("/other")
  end
  R->>R: joinPaths(outputBase?, rewrittenPath)
  R-->>C: Route using final path
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • schiller-manuel
  • Sheraff

Poem

I nibble slashes, trim the trail,
Normalize each tiny trail.
Exact or prefix, I peel away,
So routes find home and never stray.
Hop, hop — the basepath’s set to play. 🐇✨

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 clearly specifies that the fix addresses the rewriteBasepath utility failing when a trailing slash is omitted and accurately reflects the main change in the code without extra noise.
Linked Issues Check ✅ Passed The pull request implements explicit normalization for basepath slashes and rewrites input and output paths to resolve the regression described in issue #5221, and the comprehensive tests confirm the expected behavior for both trailing slash and no-trailing-slash scenarios.
Out of Scope Changes Check ✅ Passed All changes are focused on enhancing and testing the rewriteBasepath logic in the router-core package and corresponding react-router tests, without introducing modifications unrelated to the basepath handling regression.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7771c00 and ea4b76b.

📒 Files selected for processing (1)
  • packages/router-core/src/rewrite.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript in strict mode with extensive type safety across the codebase

Files:

  • packages/router-core/src/rewrite.ts
packages/router-core/**

📄 CodeRabbit inference engine (AGENTS.md)

Keep framework-agnostic core router logic in packages/router-core/

Files:

  • packages/router-core/src/rewrite.ts

@schiller-manuel
Copy link
Contributor

thanks for the PR!
can you please add some tests to the base path tests in packages/react-router/tests/router.test.tsx ?

@thenglong thenglong force-pushed the fix/rewrite-basepath-trailing-slash-not-working-without-trailing-slash branch from ea4b76b to ee96896 Compare September 26, 2025 05:02
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: 0

🧹 Nitpick comments (4)
packages/router-core/src/rewrite.ts (2)

31-40: Minor: fast‑path empty basepath and avoid needless work

Guarding for empty basepath avoids computing lowercase variants and double‑slash checks for every call.

Apply this diff:

     input: ({ url }) => {
+      // No-op when basepath is empty (common in tests/configs)
+      if (!trimmedBasepath) return url
       const pathname = opts.caseSensitive
         ? url.pathname
         : url.pathname.toLowerCase()

41-47: Handle duplicated basepath prefixes in one pass (eg. /app/app → /)

When history contains a duplicated basepath (a common artifact when navigating to “/” from a nested route), current logic strips only one prefix and may leave /app, which typically 404s. Fold the exact second prefix to / in the same pass.

Apply this diff:

       } else if (pathname.startsWith(checkBasepathWithSlash)) {
-        // Handle basepath with trailing content (e.g., /my-app/users -> /users)
-        url.pathname = url.pathname.slice(normalizedBasepath.length)
+        // Handle basepath with trailing content (e.g., /my-app/users -> /users)
+        const after = url.pathname.slice(normalizedBasepath.length)
+        // If the remainder is exactly the basepath again (e.g., /my-app/my-app), normalize to '/'
+        if (
+          (!opts.caseSensitive && after.toLowerCase() === checkBasepath) ||
+          (opts.caseSensitive && after === checkBasepath)
+        ) {
+          url.pathname = '/'
+        } else {
+          url.pathname = after
+        }
       }

Suggested test to verify the regression in the linked issue is fully covered (place under “rewriteBasepath utility”):

it('does not duplicate basepath when navigating to "/" from a nested route', async () => {
  const root = createRootRoute({
    component: () => (
      <div>
        <Link to="/" data-testid="home-link">Home</Link>
        <Outlet />
      </div>
    ),
  })
  const users = createRoute({ getParentRoute: () => root, path: '/users', component: () => <div>Users</div> })
  const router = createRouter({
    routeTree: root.addChildren([users]),
    history: createMemoryHistory({ initialEntries: ['/my-app/users'] }),
    rewrite: rewriteBasepath({ basepath: '/my-app' }),
  })
  render(<RouterProvider router={router} />)
  fireEvent.click(await screen.findByTestId('home-link'))
  await waitFor(() => expect(router.state.location.pathname).toBe('/'))
  expect(router.history.location.pathname).toBe('/my-app')
})
packages/react-router/tests/router.test.tsx (2)

2763-2881: Add a case for navigating to "/" from a nested route (dup basepath guard)

To prevent regressions like /app/app, add a test that starts at /my-app/users, clicks a Link to "/", and asserts history becomes /my-app and router state "/".

Here’s a minimal test you can add in this describe block:

it('navigating to "/" from nested route does not produce /my-app/app', async () => {
  const root = createRootRoute({
    component: () => (
      <div>
        <Link to="/" data-testid="home-link">Home</Link>
        <Outlet />
      </div>
    ),
  })
  const users = createRoute({ getParentRoute: () => root, path: '/users', component: () => <div data-testid="users">Users</div> })
  const routeTree = root.addChildren([users])
  const history = createMemoryHistory({ initialEntries: ['/my-app/users'] })
  const router = createRouter({ routeTree, history, rewrite: rewriteBasepath({ basepath: '/my-app' }) })
  render(<RouterProvider router={router} />)
  fireEvent.click(await screen.findByTestId('home-link'))
  await waitFor(() => expect(router.state.location.pathname).toBe('/'))
  expect(history.location.pathname).toBe('/my-app')
})

2883-2927: Consider adding case‑sensitivity and metacharacter basepath tests

  • Default (case-insensitive): basepath 'app' should rewrite '/APP/users' to '/users'.
  • With caseSensitive: true: basepath 'APP' should not rewrite '/app/users'.
  • Metacharacters: basepath 'v1.0' should rewrite '/v1.0/users' to '/users'.

These ensure the non‑regex implementation is robust across real‑world inputs.

Example additions:

it('default is case-insensitive', async () => {
  const root = createRootRoute({ component: () => <Outlet /> })
  const users = createRoute({ getParentRoute: () => root, path: '/users', component: () => <div data-testid="users">Users</div> })
  const router = createRouter({
    routeTree: root.addChildren([users]),
    history: createMemoryHistory({ initialEntries: ['/APP/users'] }),
    rewrite: rewriteBasepath({ basepath: 'app' }),
  })
  render(<RouterProvider router={router} />)
  await waitFor(() => expect(screen.getByTestId('users')).toBeInTheDocument())
  expect(router.state.location.pathname).toBe('/users')
})

it('caseSensitive=true requires exact case', async () => {
  const root = createRootRoute({ component: () => <Outlet /> })
  const users = createRoute({ getParentRoute: () => root, path: '/users', component: () => <div data-testid="users">Users</div> })
  const router = createRouter({
    routeTree: root.addChildren([users]),
    history: createMemoryHistory({ initialEntries: ['/app/users'] }),
    rewrite: rewriteBasepath({ basepath: 'APP', caseSensitive: true }),
  })
  render(<RouterProvider router={router} />)
  // Should not rewrite, so users component should not render
  await act(() => router.latestLoadPromise)
  expect(screen.queryByTestId('users')).toBeNull()
  expect(router.state.statusCode).toBe(404)
})

it('handles basepath with dots (metacharacters in old regex impl)', async () => {
  const root = createRootRoute({ component: () => <Outlet /> })
  const users = createRoute({ getParentRoute: () => root, path: '/users', component: () => <div data-testid="users">Users</div> })
  const router = createRouter({
    routeTree: root.addChildren([users]),
    history: createMemoryHistory({ initialEntries: ['/v1.0/users'] }),
    rewrite: rewriteBasepath({ basepath: 'v1.0' }),
  })
  render(<RouterProvider router={router} />)
  await waitFor(() => expect(screen.getByTestId('users')).toBeInTheDocument())
  expect(router.state.location.pathname).toBe('/users')
})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea4b76b and ee96896.

📒 Files selected for processing (2)
  • packages/react-router/tests/router.test.tsx (1 hunks)
  • packages/router-core/src/rewrite.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript in strict mode with extensive type safety across the codebase

Files:

  • packages/react-router/tests/router.test.tsx
  • packages/router-core/src/rewrite.ts
packages/{react-router,solid-router}/**

📄 CodeRabbit inference engine (AGENTS.md)

Implement React and Solid bindings/components only in packages/react-router/ and packages/solid-router/

Files:

  • packages/react-router/tests/router.test.tsx
packages/router-core/**

📄 CodeRabbit inference engine (AGENTS.md)

Keep framework-agnostic core router logic in packages/router-core/

Files:

  • packages/router-core/src/rewrite.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: nlynzaad
PR: TanStack/router#5182
File: e2e/react-router/basic-file-based/src/routes/non-nested/named/$baz_.bar.tsx:3-5
Timestamp: 2025-09-22T00:56:49.237Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments (e.g., `$baz_` becomes `baz` in generated types) but should be preserved in base path segments. This is the correct behavior as of the fix in PR #5182.
Learnt from: nlynzaad
PR: TanStack/router#5182
File: e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts:167-172
Timestamp: 2025-09-22T00:56:53.426Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments during path parsing, but preserved in base path segments. This is the expected behavior implemented in PR #5182.
🧬 Code graph analysis (1)
packages/react-router/tests/router.test.tsx (2)
packages/history/src/index.ts (1)
  • createMemoryHistory (568-614)
packages/router-core/src/rewrite.ts (1)
  • rewriteBasepath (21-55)
🔇 Additional comments (2)
packages/router-core/src/rewrite.ts (1)

26-28: Good move: dropped regex in favor of string checks

This eliminates regex metacharacter pitfalls and ReDoS risk while improving readability. Addresses prior review concerns about escaping and complexity.

packages/react-router/tests/router.test.tsx (1)

2673-2761: Nice coverage for basepath slash variants

These cases ensure both leading and trailing slash permutations behave consistently with input rewriting.

@schiller-manuel
Copy link
Contributor

the tests dont fail with the current implementation, so something is off here (most likely the tests...)

@thenglong
Copy link
Author

the tests dont fail with the current implementation, so something is off here (most likely the tests...)

I'll do a recheck on those test.

@omridevk
Copy link

@thenglong I also noticed that throw redirect ignores the basepath as well, didn't see a test here, might be useful to add one for that as well? wdyt @schiller-manuel

@schiller-manuel
Copy link
Contributor

@omridevk please create a new github issue for this including a complete minimal reproducer

@omridevk
Copy link

@schiller-manuel
Thanks for the quick response, opened a new issue here:
#5300

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.

basepath handling is broken in v1.132.x
3 participants