Skip to content

Conversation

@Sheraff
Copy link
Contributor

@Sheraff Sheraff commented Nov 14, 2025

Using a stateful regex w/ .exec() we can decode a string chunk by chunk while avoiding strings in the ignore list. This is less code and more performant.

bench: ~1.5x faster

 ✓  @tanstack/router-core  tests/decode.bench.ts > decodePath 1300ms
     name                    hz     min     max    mean     p75     p99    p995    p999     rme  samples
   · old decodePath  298,375.87  0.0030  0.7446  0.0034  0.0034  0.0045  0.0046  0.0087  ±0.31%   149188
   · new decodePath  450,487.87  0.0020  0.0934  0.0022  0.0023  0.0028  0.0029  0.0032  ±0.09%   225244

 BENCH  Summary

   @tanstack/router-core  new decodePath - tests/decode.bench.ts > decodePath
    1.51x faster than old decodePath

Summary by CodeRabbit

  • Refactor

    • Improved URL/path decoding in the router for more reliable handling of unusual encodings and edge cases, reducing decoding errors and improving navigation stability.
    • Switched to a more robust, iterative decoding approach for predictable behavior across different path formats.
  • Tests

    • Updated test expectations to standardize percent-encoding casing for consistent validation.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 14, 2025

Walkthrough

Refactored path decoding in router-core from a recursive, ignore-list-driven flow to an iterative segment-parsing approach. decodePath signature changed to decodePath(path: string, decodeIgnore?: Array<string>); decodeSegment replaces prior helper and tests updated for lowercase hex expectations.

Changes

Cohort / File(s) Summary
Core Decoding Refactor
packages/router-core/src/utils.ts
Replaced recursive split-and-decode pipeline with iterative cursor-based parsing; removed DECODE_IGNORE_LIST; added decodeSegment which first tries decodeURI then falls back to manual hex-decoding (case-insensitive); updated decodePath signature and ignore handling (optional array, builds RegExp).
Test Expectation Updates
packages/router-core/tests/utils.test.ts
Updated expected decoded path values to use lowercase hex digits in percent-encodings (e.g., %2F%2f, %5CAh%5cAh) to match new decoding normalization.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Caller
    participant decodePath
    participant decodeSegment
    note right of decodePath `#DDFFDD`: New iterative flow

    Caller->>decodePath: decodePath(path, decodeIgnore?)
    decodePath->>decodeSegment: extract next segment (cursor..match)
    decodeSegment-->>decodePath: decodedSegment (or preserved)
    decodePath->>decodeSegment: continue until end
    decodePath-->>Caller: reconstructed decoded path
Loading
sequenceDiagram
    autonumber
    participant Caller
    participant decodePath_old as decodePath (old)
    participant splitAndDecode

    note right of decodePath_old `#FFEECC`: Old recursive flow

    Caller->>decodePath_old: decodePath(part, decodeIgnore)
    decodePath_old->>splitAndDecode: splitAndDecode(parts...)
    splitAndDecode-->>decodePath_old: decoded parts (recursive)
    decodePath_old-->>Caller: decoded path
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Pay attention to callers of decodePath for signature change (partpath, optional decodeIgnore).
  • Verify regex construction for decodeIgnore and cursor handling across edge cases (leading/trailing matches, consecutive matches).
  • Inspect fallback hex-decoding for correctness and case-insensitivity.

Possibly related PRs

Suggested reviewers

  • schiller-manuel
  • nlynzaad

Poem

🐰 Down the path where percent signs play,
I nibble hex and chase the stray,
From nested calls I hop away,
Iteration lights my trail today,
Lowercase footprints guide the way ✨

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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: refactoring decodePath to be simpler and higher-performing, which aligns with the core modifications shown in the changeset.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-router-core-decode-path-regex

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

@nx-cloud
Copy link

nx-cloud bot commented Nov 14, 2025

View your CI Pipeline Execution ↗ for commit 07dbfea

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 7m 2s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 17s View ↗

☁️ Nx Cloud last updated this comment at 2025-11-14 19:05:51 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 14, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@5867

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@5867

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@5867

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@5867

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/nitro-v2-vite-plugin@5867

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@5867

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@5867

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@5867

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@5867

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@5867

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@5867

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@5867

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@5867

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@5867

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@5867

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@5867

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@5867

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@5867

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@5867

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@5867

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@5867

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@5867

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@5867

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-ssr-query@5867

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@5867

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@5867

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@5867

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@5867

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@5867

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@5867

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-static-server-functions@5867

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@5867

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@5867

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@5867

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@5867

commit: 07dbfea

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 a096ca1 and 335ba30.

📒 Files selected for processing (2)
  • packages/router-core/src/utils.ts (2 hunks)
  • packages/router-core/tests/utils.test.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: nlynzaad
Repo: TanStack/router PR: 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.
Learnt from: nlynzaad
Repo: TanStack/router PR: 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: 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.
📚 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/utils.ts
  • packages/router-core/tests/utils.test.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/utils.test.ts
📚 Learning: 2025-09-22T00:56:49.237Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 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.

Applied to files:

  • packages/router-core/tests/utils.test.ts
🪛 ast-grep (0.39.9)
packages/router-core/src/utils.ts

[warning] 508-508: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(${decodeIgnore.join('|')}, 'gi')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (3)
packages/router-core/src/utils.ts (2)

492-505: LGTM! Robust fallback strategy.

The two-tier error handling (try full decode, then fall back to individual sequences) elegantly handles malformed percent-encodings while preserving valid ones.


507-521: Iterative approach improves clarity and performance.

The refactored implementation is cleaner and more efficient than a recursive approach. The cursor-based iteration correctly preserves ignored sequences while decoding the rest.

packages/router-core/tests/utils.test.ts (1)

608-614: Test correctly reflects case-preserving behavior.

The updated expectation confirms that decodePath now preserves the original casing of ignored percent-encodings (e.g., both %2f and %2F are preserved as-is). This is consistent with the case-insensitive regex matching in the implementation.

@Sheraff Sheraff requested a review from nlynzaad November 14, 2025 18:46
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

♻️ Duplicate comments (1)
packages/router-core/src/utils.ts (1)

507-519: Fix empty decodeIgnore handling and escape dynamic regex input

Two issues in decodePath’s new implementation:

  1. Empty decodeIgnore breaks decoding logic

When decodeIgnore is an empty array, new RegExp(decodeIgnore.join('|'), 'gi') becomes new RegExp('', 'gi'). With a zero-length global pattern, re.exec(path) repeatedly matches empty strings, so every path.slice(cursor, match.index) is empty and the final cursor ends up past the end of the string. The function then returns an empty string for any non-empty path, which is almost certainly unintended.

Guard against this by treating an empty array like “no custom ignore,” e.g.:

-export function decodePath(path: string, decodeIgnore?: Array<string>): string {
+export function decodePath(path: string, decodeIgnore?: Array<string>): string {
   if (!path) return path
-  const re = decodeIgnore
-    ? new RegExp(`${decodeIgnore.join('|')}`, 'gi')
-    : /%25|%5C/gi
+  const re =
+    decodeIgnore && decodeIgnore.length
+      ? new RegExp(decodeIgnore.join('|'), 'gi')
+      : /%25|%5C/gi
  1. Dynamic regex from decodeIgnore should be escaped

As already noted in an earlier review, constructing the RegExp directly from decodeIgnore.join('|') means any regex metacharacters in those strings change the pattern’s meaning and could open the door to surprising matches or ReDoS if values ever become user-controlled.

A defensive fix is to escape each entry before joining:

-      ? new RegExp(decodeIgnore.join('|'), 'gi')
+      ? new RegExp(
+          decodeIgnore
+            .map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
+            .join('|'),
+          'gi',
+        )

(or use RegExp.escape when your TS/JS targets support it).

This keeps the performance characteristics of the new implementation while hardening behavior for future callers.

🧹 Nitpick comments (1)
packages/router-core/src/utils.ts (1)

492-505: decodeSegment fallback looks robust; minor compatibility nit around replaceAll

The two-step decoding strategy (decodeURI first, then per-%XX byte fallback) is a sensible way to preserve malformed sequences while still recovering what can be decoded. Only minor consideration: String.prototype.replaceAll is ES2021+, so if you still support older runtimes without transpilation/polyfills, you may want to switch to .replace with a global regex for broader compatibility.

For example:

-    return segment.replaceAll(/%[0-9A-F]{2}/gi, (match) => {
+    return segment.replace(/%[0-9A-F]{2}/gi, (match) => {
       try {
         return decodeURI(match)
       } catch {
         return match
       }
     })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 335ba30 and 07dbfea.

📒 Files selected for processing (1)
  • packages/router-core/src/utils.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: nlynzaad
Repo: TanStack/router PR: 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
Repo: TanStack/router PR: 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.
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.
📚 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/utils.ts
🧬 Code graph analysis (1)
packages/router-core/src/utils.ts (1)
packages/router-core/src/index.ts (1)
  • decodePath (281-281)
🪛 ast-grep (0.39.9)
packages/router-core/src/utils.ts

[warning] 509-509: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(${decodeIgnore.join('|')}, 'gi')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview

Copy link
Contributor

@nlynzaad nlynzaad left a comment

Choose a reason for hiding this comment

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

This looks great, was looking for a way to just walk the string forward, but didn't think of this way. 🚀

@Sheraff Sheraff merged commit 18edb31 into main Nov 14, 2025
6 checks passed
@Sheraff Sheraff deleted the refactor-router-core-decode-path-regex branch November 14, 2025 19:46
roduyemi pushed a commit to roduyemi/oss-router that referenced this pull request Nov 19, 2025
…rformance (TanStack#5867)

* refactor(router-core): simplify decodePath implementation, improve performance

* simplify more
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.

4 participants