diff --git a/.changeset/kind-coins-serve.md b/.changeset/kind-coins-serve.md new file mode 100644 index 0000000000..82a382bd5b --- /dev/null +++ b/.changeset/kind-coins-serve.md @@ -0,0 +1,9 @@ +--- +"react-router": patch +--- + +Fix RSC double slashes in manifest URLs + +Normalize double slashes in getManifestUrl function to prevent ERR_NAME_NOT_RESOLVED errors when URLs contain double slashes like //en//test2/test + + diff --git a/contributors.yml b/contributors.yml index c1a62e6103..ab327719bd 100644 --- a/contributors.yml +++ b/contributors.yml @@ -459,3 +459,4 @@ - zeromask1337 - zheng-chuang - zxTomw +- swarnim02 diff --git a/packages/react-router/__tests__/rsc-double-slashes-test.ts b/packages/react-router/__tests__/rsc-double-slashes-test.ts new file mode 100644 index 0000000000..ab6f9ae9de --- /dev/null +++ b/packages/react-router/__tests__/rsc-double-slashes-test.ts @@ -0,0 +1,20 @@ +import { joinPaths } from "../lib/router/utils"; + +describe("joinPaths double slash normalization", () => { + it("normalizes double slashes in single path", () => { + expect(joinPaths(["//en//test2/test"])).toBe("/en/test2/test"); + expect(joinPaths(["/app//base/"])).toBe("/app/base/"); + expect(joinPaths(["///multiple///slashes"])).toBe("/multiple/slashes"); + }); + + it("normalizes double slashes in multiple paths", () => { + expect(joinPaths(["//en//test1", "//fr//test2"])).toBe("/en/test1/fr/test2"); + expect(joinPaths(["path//with//double", "slashes//here"])).toBe("path/with/double/slashes/here"); + }); + + it("preserves normal paths", () => { + expect(joinPaths(["/normal/path"])).toBe("/normal/path"); + expect(joinPaths(["path/without/leading/slash"])).toBe("path/without/leading/slash"); + expect(joinPaths([""])).toBe(""); + }); +}); \ No newline at end of file diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index f90aba951f..cb1015c60f 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -1777,8 +1777,12 @@ export function resolveTo( return path; } -export const joinPaths = (paths: string[]): string => - paths.join("/").replace(/\/\/+/g, "/"); +export const joinPaths = (paths: string[]): string => { + return paths + .map(path => path.replace(/\/\/+/g, "/")) // Normalize double slashes within each path + .join("/") + .replace(/\/\/+/g, "/"); // Normalize any double slashes created by joining +}; export const normalizePathname = (pathname: string): string => pathname.replace(/\/+$/, "").replace(/^\/*/, "/"); diff --git a/packages/react-router/lib/rsc/browser.tsx b/packages/react-router/lib/rsc/browser.tsx index 5046e28179..78bc7428bd 100644 --- a/packages/react-router/lib/rsc/browser.tsx +++ b/packages/react-router/lib/rsc/browser.tsx @@ -22,7 +22,7 @@ import type { DataStrategyFunctionArgs, RouterContextProvider, } from "../router/utils"; -import { ErrorResponseImpl, createContext } from "../router/utils"; +import { ErrorResponseImpl, createContext, joinPaths } from "../router/utils"; import type { DecodedSingleFetchResults, FetchAndDecodeFunction, @@ -972,7 +972,9 @@ function getManifestUrl(paths: string[]): URL | null { } if (paths.length === 1) { - return new URL(`${paths[0]}.manifest`, window.location.origin); + // Normalize double slashes in the single path + const normalizedPath = joinPaths([paths[0]]); + return new URL(`${normalizedPath}.manifest`, window.location.origin); } const globalVar = window as WindowWithRouterGlobals; @@ -980,8 +982,12 @@ function getManifestUrl(paths: string[]): URL | null { /^\/|\/$/g, "", ); + // Normalize double slashes in basename + basename = joinPaths([basename]); let url = new URL(`${basename}/.manifest`, window.location.origin); - url.searchParams.set("paths", paths.sort().join(",")); + // Normalize double slashes in all paths before joining + const normalizedPaths = paths.map(path => joinPaths([path])); + url.searchParams.set("paths", normalizedPaths.sort().join(",")); return url; }