Skip to content

SSR redirect loop for splat routes with URL-encoded characters (%20, parentheses) #6519

@thejasonxie

Description

@thejasonxie

Hi! 👋

Firstly, thanks for your work on this project! 🙂

When using splat routes (/$) with URLs containing encoded special characters (e.g., %20 for spaces, %28/%29 for parentheses), SSR causes an infinite 307 redirect loop.

Root cause: The encodeParam function in path.ts uses encodeURI() for splat params, which doesn't encode spaces or parentheses. When comparing the original URL (/path/file%20name.pdf) with the rebuilt URL (/path/file name.pdf), they don't match, triggering a redirect with unencoded characters in the Location header.

Reproduction:

  1. Create a splat route ($.tsx)
  2. Navigate to a URL with spaces: /owner/repo/blob/main/my%20file.pdf
  3. Refresh the page
  4. Observe infinite 307 redirect loop

Fix: Replace encodeURI(value) with value.split('/').map(segment => encodeURIComponent(segment)).join('/') to properly encode each path segment.

Today I used patch-package to patch @tanstack/router-core@1.157.3 for the project I'm working on.

Here is the diff that solved my problem:

diff --git a/node_modules/@tanstack/router-core/dist/esm/path.js b/node_modules/@tanstack/router-core/dist/esm/path.js
index b178989..af2c810 100644
--- a/node_modules/@tanstack/router-core/dist/esm/path.js
+++ b/node_modules/@tanstack/router-core/dist/esm/path.js
@@ -120,7 +120,9 @@ function encodeParam(key, params, decoder) {
   const value = params[key];
   if (typeof value !== "string") return value;
   if (key === "_splat") {
-    return encodeURI(value);
+    // Use encodeURIComponent for each segment to properly encode spaces and special characters
+    // but preserve forward slashes as path separators
+    return value.split('/').map(segment => encodeURIComponent(segment)).join('/');
   } else {
     return encodePathParam(value, decoder);
   }
diff --git a/node_modules/@tanstack/router-core/dist/esm/utils.js b/node_modules/@tanstack/router-core/dist/esm/utils.js
index d966d48..1b1469c 100644
--- a/node_modules/@tanstack/router-core/dist/esm/utils.js
+++ b/node_modules/@tanstack/router-core/dist/esm/utils.js
@@ -219,8 +219,24 @@ function decodePath(path, decodeIgnore) {
   return result;
 }
 function encodeNonAscii(path) {
-  if (!/[^\u0000-\u007F]/.test(path)) return path;
-  return path.replace(/[^\u0000-\u007F]/gu, encodeURIComponent);
+  // Encode both non-ASCII characters and special characters that should be encoded in URLs
+  // Split by / to preserve path separators, encode each segment
+  return path.split('/').map(segment => {
+    // Encode special characters: spaces, parentheses, brackets, etc.
+    if (/[\s()[\]{}#]/.test(segment)) {
+      return segment.split('').map(char => {
+        if (/[\s()[\]{}#]/.test(char)) {
+          return encodeURIComponent(char);
+        }
+        return char;
+      }).join('');
+    }
+    // Also encode non-ASCII
+    if (/[^\u0000-\u007F]/.test(segment)) {
+      return segment.replace(/[^\u0000-\u007F]/gu, encodeURIComponent);
+    }
+    return segment;
+  }).join('/');
 }
 function buildDevStylesUrl(basepath, routeIds) {
   const trimmedBasepath = basepath.replace(/^\/+|\/+$/g, "");

This issue body was partially generated by patch-package.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions