Skip to content

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

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.

Activity

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

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