diff --git a/.changeset/fix-colon-in-relative-paths.md b/.changeset/fix-colon-in-relative-paths.md new file mode 100644 index 0000000000..feb73f5f27 --- /dev/null +++ b/.changeset/fix-colon-in-relative-paths.md @@ -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 diff --git a/contributors.yml b/contributors.yml index 38278eb0c9..aeba29b05d 100644 --- a/contributors.yml +++ b/contributors.yml @@ -1,3 +1,4 @@ +- 0372hoanghoccode - 0xEddie - 3fuyang - 43081j diff --git a/packages/react-router/__tests__/router/resolveTo-test.tsx b/packages/react-router/__tests__/router/resolveTo-test.tsx index d960f0a88c..89a73e7d34 100644 --- a/packages/react-router/__tests__/router/resolveTo-test.tsx +++ b/packages/react-router/__tests__/router/resolveTo-test.tsx @@ -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"); + }); }); diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index 4a4a79593e..bed471a3d0 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -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); /**