From fbd3ef876ffde9a5c48a5c87eb88d7ab71b1b075 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 9 Jan 2026 12:27:36 -0500 Subject: [PATCH 1/2] Fix double slash normalization or useNavigate colon paths --- .changeset/blue-parrots-grin.md | 5 +++++ .../__tests__/resolvePath-test.tsx | 16 --------------- packages/react-router/lib/router/utils.ts | 20 ++++--------------- 3 files changed, 9 insertions(+), 32 deletions(-) create mode 100644 .changeset/blue-parrots-grin.md diff --git a/.changeset/blue-parrots-grin.md b/.changeset/blue-parrots-grin.md new file mode 100644 index 0000000000..6cb6da89a0 --- /dev/null +++ b/.changeset/blue-parrots-grin.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Fix double slash normalization for useNavigate colon urls diff --git a/packages/react-router/__tests__/resolvePath-test.tsx b/packages/react-router/__tests__/resolvePath-test.tsx index 22719de070..261e7f2119 100644 --- a/packages/react-router/__tests__/resolvePath-test.tsx +++ b/packages/react-router/__tests__/resolvePath-test.tsx @@ -1,22 +1,6 @@ import { resolvePath } from "react-router"; describe("resolvePath", () => { - it("does not touch with protocol-less absolute paths", () => { - expect(resolvePath("//google.com")).toMatchObject({ - pathname: "//google.com", - }); - - expect(resolvePath("//google.com/../../path")).toMatchObject({ - pathname: "//google.com/../../path", - }); - - expect(resolvePath("//google.com?q=query#hash")).toMatchObject({ - pathname: "//google.com", - search: "?q=query", - hash: "#hash", - }); - }); - it('resolves absolute paths irrespective of the "from" pathname', () => { expect(resolvePath("/search", "/inbox")).toMatchObject({ pathname: "/search", diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index 4a4a79593e..41cc4be463 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -1603,23 +1603,11 @@ export function resolvePath(to: To, fromPathname = "/"): Path { let pathname: string; if (toPathname) { - if (isAbsoluteUrl(toPathname)) { - pathname = toPathname; + toPathname = toPathname.replace(/\/\/+/g, "/"); + if (toPathname.startsWith("/")) { + pathname = resolvePathname(toPathname.substring(1), "/"); } else { - if (toPathname.includes("//")) { - let oldPathname = toPathname; - toPathname = toPathname.replace(/\/\/+/g, "/"); - warning( - false, - `Pathnames cannot have embedded double slashes - normalizing ` + - `${oldPathname} -> ${toPathname}`, - ); - } - if (toPathname.startsWith("/")) { - pathname = resolvePathname(toPathname.substring(1), "/"); - } else { - pathname = resolvePathname(toPathname, fromPathname); - } + pathname = resolvePathname(toPathname, fromPathname); } } else { pathname = fromPathname; From 0571de614f3d38f7a84c842f79c5459b389da260 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 16 Jan 2026 10:15:10 -0500 Subject: [PATCH 2/2] Add test --- .../__tests__/resolvePath-test.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/react-router/__tests__/resolvePath-test.tsx b/packages/react-router/__tests__/resolvePath-test.tsx index 261e7f2119..6a28d83e41 100644 --- a/packages/react-router/__tests__/resolvePath-test.tsx +++ b/packages/react-router/__tests__/resolvePath-test.tsx @@ -63,6 +63,32 @@ describe("resolvePath", () => { spy.mockRestore(); }); + it("handles relative paths with an embedded colon", () => { + expect(resolvePath("foo:bar", "/")).toMatchObject({ + pathname: "/foo:bar", + }); + + expect(resolvePath("./foo:bar", "/")).toMatchObject({ + pathname: "/foo:bar", + }); + + expect(resolvePath("../foo:bar", "/")).toMatchObject({ + pathname: "/foo:bar", + }); + + expect(resolvePath("foo:bar", "/path")).toMatchObject({ + pathname: "/path/foo:bar", + }); + + expect(resolvePath("./foo:bar", "/path")).toMatchObject({ + pathname: "/path/foo:bar", + }); + + expect(resolvePath("../foo:bar", "/path")).toMatchObject({ + pathname: "/foo:bar", + }); + }); + it('ignores trailing slashes on the "from" pathname when resolving relative paths', () => { expect(resolvePath("../search", "/inbox/")).toMatchObject({ pathname: "/search",