Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/fix-colon-in-relative-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"react-router": patch
"react-router-dom": patch
"@react-router/dev": patch
"@react-router/cloudflare": patch
"@react-router/node": patch
"@react-router/serve": patch
"@react-router/fs-routes": patch
"@react-router/express": patch
"@react-router/architect": patch
"@react-router/remix-routes-option-adapter": patch
"create-react-router": patch
---

Fix regression in v7.9.6 where relative paths with colons (like `my-path:value`) were incorrectly treated as absolute URLs. The router now correctly distinguishes between actual absolute URLs (like `mailto:`, `tel:`, `http://`) and relative paths containing colons.

Fixes #14711
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- 0372hoanghoccode
- 0xEddie
- 3fuyang
- 43081j
Expand Down
58 changes: 58 additions & 0 deletions packages/react-router/__tests__/router/resolveTo-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,62 @@ describe("resolveTo", () => {
hash: "",
});
});

it("should handle relative paths with colons", () => {
const routePathnames = ["/", "/base"];
const locationPathname = "/base";

// Paths with colons should be resolved as relative paths
let resolvedPath = resolveTo(
{ pathname: "my-path:with-colon" },
routePathnames,
locationPathname,
);
expect(resolvedPath.pathname).toBe("/base/my-path:with-colon");

// Another example with different colon pattern
resolvedPath = resolveTo(
{ pathname: "item:123" },
routePathnames,
locationPathname,
);
expect(resolvedPath.pathname).toBe("/base/item:123");

// Prefixing with ./ should also work
resolvedPath = resolveTo(
{ pathname: "./my-path:with-colon" },
routePathnames,
locationPathname,
);
expect(resolvedPath.pathname).toBe("/base/my-path:with-colon");
});

it("should still recognize actual absolute URLs", () => {
const routePathnames = ["/", "/base"];
const locationPathname = "/base";

// Hierarchical URLs with ://
let resolvedPath = resolveTo(
{ pathname: "http://localhost" },
routePathnames,
locationPathname,
);
expect(resolvedPath.pathname).toBe("http://localhost");

// Non-hierarchical schemes like mailto: should be treated as absolute URLs
resolvedPath = resolveTo(
{ pathname: "mailto:test@example.com" },
routePathnames,
locationPathname,
);
expect(resolvedPath.pathname).toBe("mailto:test@example.com");

// Protocol-relative URLs
resolvedPath = resolveTo(
{ pathname: "//example.com/path" },
routePathnames,
locationPathname,
);
expect(resolvedPath.pathname).toBe("//example.com/path");
});
});
6 changes: 5 additions & 1 deletion packages/react-router/lib/router/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1581,7 +1581,11 @@ export function prependBasename({
return pathname === "/" ? basename : joinPaths([basename, pathname]);
}

const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
// Match absolute URLs: hierarchical (scheme://), protocol-relative (//),
// and common non-hierarchical schemes (mailto:, tel:, etc.)
// This allows relative paths with colons like "my-path:value" to be resolved
// as relative paths without requiring a ./ prefix
const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:\/\/|\/\/|(?:mailto|tel|sms|data|blob|file|about|javascript|vbscript|web\+[a-z0-9+.-]*|app|chrome|chrome-extension|moz-extension|ms-browser-extension):)/i;
export const isAbsoluteUrl = (url: string) => ABSOLUTE_URL_REGEX.test(url);

/**
Expand Down