Skip to content

Commit 02e65a5

Browse files
authored
Normalize double-slashes in resolvePath (#14529)
1 parent 3d9e7cc commit 02e65a5

File tree

5 files changed

+87
-9
lines changed

5 files changed

+87
-9
lines changed

.changeset/nice-goats-shout.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Normalize double-slashes in `resolvePath`

packages/react-router/__tests__/resolvePath-test.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,38 @@
11
import { resolvePath } from "react-router";
22

33
describe("resolvePath", () => {
4+
it("does not touch with protocol-less absolute paths", () => {
5+
expect(resolvePath("//google.com")).toMatchObject({
6+
pathname: "//google.com",
7+
});
8+
9+
expect(resolvePath("//google.com/../../path")).toMatchObject({
10+
pathname: "//google.com/../../path",
11+
});
12+
13+
expect(resolvePath("//google.com?q=query#hash")).toMatchObject({
14+
pathname: "//google.com",
15+
search: "?q=query",
16+
hash: "#hash",
17+
});
18+
});
19+
420
it('resolves absolute paths irrespective of the "from" pathname', () => {
521
expect(resolvePath("/search", "/inbox")).toMatchObject({
622
pathname: "/search",
723
});
24+
25+
expect(resolvePath("/search/../123", "/inbox")).toMatchObject({
26+
pathname: "/123",
27+
});
28+
29+
expect(resolvePath("/search/../../123", "/inbox")).toMatchObject({
30+
pathname: "/123",
31+
});
32+
33+
expect(resolvePath("/search/user/../../123", "/inbox")).toMatchObject({
34+
pathname: "/123",
35+
});
836
});
937

1038
it("resolves relative paths", () => {
@@ -23,6 +51,32 @@ describe("resolvePath", () => {
2351
expect(resolvePath("search", "/inbox")).toMatchObject({
2452
pathname: "/inbox/search",
2553
});
54+
55+
expect(resolvePath("search/../123", "/inbox")).toMatchObject({
56+
pathname: "/inbox/123",
57+
});
58+
59+
expect(resolvePath("search/../../123", "/inbox")).toMatchObject({
60+
pathname: "/123",
61+
});
62+
63+
expect(resolvePath("search/../../../123", "/inbox")).toMatchObject({
64+
pathname: "/123",
65+
});
66+
});
67+
68+
it("normalizes any mid-path double-slashes", () => {
69+
let spy = jest.spyOn(console, "warn").mockImplementation(() => {});
70+
71+
expect(resolvePath("/search/../..//foo")).toMatchObject({
72+
pathname: "/foo",
73+
});
74+
75+
expect(resolvePath("search/../..//foo", "/inbox")).toMatchObject({
76+
pathname: "/foo",
77+
});
78+
79+
spy.mockRestore();
2680
});
2781

2882
it('ignores trailing slashes on the "from" pathname when resolving relative paths', () => {

packages/react-router/lib/router/router.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
convertRoutesToDataRoutes,
5959
getPathContributingMatches,
6060
getResolveToMatches,
61+
isAbsoluteUrl,
6162
isUnsupportedLazyRouteObjectKey,
6263
isUnsupportedLazyRouteFunctionKey,
6364
isRouteErrorResponse,
@@ -838,9 +839,6 @@ export const IDLE_BLOCKER: BlockerUnblocked = {
838839
location: undefined,
839840
};
840841

841-
const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
842-
export const isAbsoluteUrl = (url: string) => ABSOLUTE_URL_REGEX.test(url);
843-
844842
const defaultMapRouteProperties: MapRoutePropertiesFunction = (route) => ({
845843
hasErrorBoundary: Boolean(route.hasErrorBoundary),
846844
});

packages/react-router/lib/router/utils.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1571,6 +1571,9 @@ export function prependBasename({
15711571
return pathname === "/" ? basename : joinPaths([basename, pathname]);
15721572
}
15731573

1574+
const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
1575+
export const isAbsoluteUrl = (url: string) => ABSOLUTE_URL_REGEX.test(url);
1576+
15741577
/**
15751578
* Returns a resolved {@link Path} object relative to the given pathname.
15761579
*
@@ -1588,11 +1591,29 @@ export function resolvePath(to: To, fromPathname = "/"): Path {
15881591
hash = "",
15891592
} = typeof to === "string" ? parsePath(to) : to;
15901593

1591-
let pathname = toPathname
1592-
? toPathname.startsWith("/")
1593-
? toPathname
1594-
: resolvePathname(toPathname, fromPathname)
1595-
: fromPathname;
1594+
let pathname: string;
1595+
if (toPathname) {
1596+
if (isAbsoluteUrl(toPathname)) {
1597+
pathname = toPathname;
1598+
} else {
1599+
if (toPathname.includes("//")) {
1600+
let oldPathname = toPathname;
1601+
toPathname = toPathname.replace(/\/\/+/g, "/");
1602+
warning(
1603+
false,
1604+
`Pathnames cannot have embedded double slashes - normalizing ` +
1605+
`${oldPathname} -> ${toPathname}`,
1606+
);
1607+
}
1608+
if (toPathname.startsWith("/")) {
1609+
pathname = resolvePathname(toPathname.substring(1), "/");
1610+
} else {
1611+
pathname = resolvePathname(toPathname, fromPathname);
1612+
}
1613+
}
1614+
} else {
1615+
pathname = fromPathname;
1616+
}
15961617

15971618
return {
15981619
pathname,

packages/react-router/lib/rsc/server.rsc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type {
1212
import { type Location } from "../router/history";
1313
import {
1414
createStaticHandler,
15-
isAbsoluteUrl,
1615
isMutationMethod,
1716
isResponse,
1817
isRedirectResponse,
@@ -26,6 +25,7 @@ import {
2625
type ShouldRevalidateFunction,
2726
type RouterContextProvider,
2827
type TrackedPromise,
28+
isAbsoluteUrl,
2929
isRouteErrorResponse,
3030
matchRoutes,
3131
prependBasename,

0 commit comments

Comments
 (0)