From f45d4ca9f3cb19db3471f8c34930a81620f1932e Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 26 Aug 2026 12:57:59 -0400 Subject: [PATCH] Revert "fix: normalize control characters in relative URLs (#15416)" (#15442) This reverts commit 36bdbeb05b7ef9e304430d14fa5eb0200b9b0762. --- .../patch.normalize-url-control-characters.md | 1 - .../__tests__/dom/link-href-test.tsx | 20 ------------------ .../__tests__/router/browser-test.ts | 6 ------ .../__tests__/router/redirects-test.ts | 21 +------------------ packages/react-router/lib/dom/lib.tsx | 7 ++++--- packages/react-router/lib/dom/server.tsx | 3 +-- packages/react-router/lib/router/history.ts | 5 ++--- packages/react-router/lib/router/router.ts | 3 --- packages/react-router/lib/router/url.ts | 19 ----------------- packages/react-router/lib/router/utils.ts | 9 ++------ packages/react-router/lib/rsc/browser.tsx | 7 +------ 11 files changed, 11 insertions(+), 90 deletions(-) delete mode 100644 packages/react-router/.changes/patch.normalize-url-control-characters.md diff --git a/packages/react-router/.changes/patch.normalize-url-control-characters.md b/packages/react-router/.changes/patch.normalize-url-control-characters.md deleted file mode 100644 index 1fcb82fe34..0000000000 --- a/packages/react-router/.changes/patch.normalize-url-control-characters.md +++ /dev/null @@ -1 +0,0 @@ -Improve handling of special characters in navigation paths diff --git a/packages/react-router/__tests__/dom/link-href-test.tsx b/packages/react-router/__tests__/dom/link-href-test.tsx index 589ec13300..861de56e30 100644 --- a/packages/react-router/__tests__/dom/link-href-test.tsx +++ b/packages/react-router/__tests__/dom/link-href-test.tsx @@ -129,26 +129,6 @@ describe(" href", () => { expect(renderer.root.findByType("a").props.href).toEqual("//remix.run"); }); - test("normalizes special characters in relative values", () => { - let renderer: TestRenderer.ReactTestRenderer; - TestRenderer.act(() => { - renderer = TestRenderer.create( - - - - } - /> - - - , - ); - }); - - expect(renderer.root.findByType("a").props.href).toEqual("/nested/path"); - }); - test(' is treated as external link', () => { let renderer: TestRenderer.ReactTestRenderer; TestRenderer.act(() => { diff --git a/packages/react-router/__tests__/router/browser-test.ts b/packages/react-router/__tests__/router/browser-test.ts index 1febdb99ff..d54373171d 100644 --- a/packages/react-router/__tests__/router/browser-test.ts +++ b/packages/react-router/__tests__/router/browser-test.ts @@ -47,12 +47,6 @@ describe("a browser history", () => { expect(href).toEqual("/the/path?the=query#the-hash"); }); - it("normalizes special characters in relative hrefs", () => { - for (let char of ["\t", "\n", "\r"]) { - expect(history.createHref(`/${char}/nested/path`)).toBe("/nested/path"); - } - }); - it("does not encode the generated path", () => { const encodedHref = history.createHref({ pathname: "/%23abc", diff --git a/packages/react-router/__tests__/router/redirects-test.ts b/packages/react-router/__tests__/router/redirects-test.ts index 8060380c7d..86d5f99d10 100644 --- a/packages/react-router/__tests__/router/redirects-test.ts +++ b/packages/react-router/__tests__/router/redirects-test.ts @@ -1,6 +1,6 @@ import { createMemoryHistory } from "../../lib/router/history"; import { IDLE_NAVIGATION, createRouter } from "../../lib/router/router"; -import { redirect, replace } from "../../lib/router/utils"; +import { replace } from "../../lib/router/utils"; import type { TestRouteObject } from "./utils/data-router-setup"; import { cleanup, setup } from "./utils/data-router-setup"; import { createFormData, tick } from "./utils/utils"; @@ -482,25 +482,6 @@ describe("redirects", () => { } }); - it("normalizes special characters in redirects", async () => { - let router = createRouter({ - history: createMemoryHistory(), - routes: [ - { path: "/" }, - { path: "/start", loader: () => redirect("/\t/parent") }, - { path: "/parent" }, - ], - }); - router.initialize(); - await tick(); - - await router.navigate("/start"); - expect(router.state.location).toMatchObject({ - pathname: "/parent", - }); - router.dispose(); - }); - it("properly handles same-origin absolute URLs when using a basename", async () => { let t = setup({ routes: REDIRECT_ROUTES, basename: "/base" }); diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index 65118ae3c5..32d5d93de6 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -38,13 +38,13 @@ import { defaultMapRouteProperties, ErrorResponseImpl, SUPPORTED_ERROR_TYPES, - isAbsoluteUrl, joinPaths, matchPath, parseToInfo, resolveTo, stripBasename, } from "../router/utils"; +import { ABSOLUTE_URL_REGEX } from "../router/url"; // eslint-disable-next-line @typescript-eslint/no-unused-vars import type * as _ from "./global"; @@ -1334,7 +1334,7 @@ export const Link = React.forwardRef( ) { let { basename, navigator, useTransitions } = React.useContext(NavigationContext); - let isAbsolute = typeof to === "string" && isAbsoluteUrl(to); + let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to); let parsed = parseToInfo(to, basename); to = parsed.to; @@ -1944,7 +1944,8 @@ export const Form = React.forwardRef( let formAction = useFormAction(action, { relative }); let formMethod: HTMLFormMethod = method.toLowerCase() === "get" ? "get" : "post"; - let isAbsolute = typeof action === "string" && isAbsoluteUrl(action); + let isAbsolute = + typeof action === "string" && ABSOLUTE_URL_REGEX.test(action); let submitHandler: React.SubmitEventHandler = (event) => { onSubmit && onSubmit(event); diff --git a/packages/react-router/lib/dom/server.tsx b/packages/react-router/lib/dom/server.tsx index 1fd29f0269..0713618bbe 100644 --- a/packages/react-router/lib/dom/server.tsx +++ b/packages/react-router/lib/dom/server.tsx @@ -24,7 +24,7 @@ import { convertRoutesToDataRoutes, isRouteErrorResponse, } from "../router/utils"; -import { ABSOLUTE_URL_REGEX, normalizeRelativeUrl } from "../router/url"; +import { ABSOLUTE_URL_REGEX } from "../router/url"; import { DataRoutes, Router } from "../components"; import { DataRouterContext, @@ -457,7 +457,6 @@ function createHref(to: To) { function encodeLocation(to: To): Path { let href = typeof to === "string" ? to : createPath(to); - href = normalizeRelativeUrl(href); // Treating this as a full URL will strip any trailing spaces so we need to // pre-encode them since they might be part of a matching splat param from // an ancestor route diff --git a/packages/react-router/lib/router/history.ts b/packages/react-router/lib/router/history.ts index b4cc9894a0..08148d6a0e 100644 --- a/packages/react-router/lib/router/history.ts +++ b/packages/react-router/lib/router/history.ts @@ -1,4 +1,4 @@ -import { normalizeRelativeUrl, PROTOCOL_RELATIVE_URL_REGEX } from "./url"; +import { PROTOCOL_RELATIVE_URL_REGEX } from "./url"; //////////////////////////////////////////////////////////////////////////////// //#region Types and Constants @@ -407,7 +407,7 @@ export function createBrowserHistory( } function createBrowserHref(window: Window, to: To) { - return normalizeRelativeUrl(typeof to === "string" ? to : createPath(to)); + return typeof to === "string" ? to : createPath(to); } return getUrlBasedHistory( @@ -798,7 +798,6 @@ export function createBrowserURLImpl( invariant(base, "No window.location.(origin|href) available to create URL"); let href = typeof to === "string" ? to : createPath(to); - href = normalizeRelativeUrl(href); // Treating this as a full URL will strip any trailing spaces so we need to // pre-encode them since they might be part of a matching splat param from diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 5b320d0b49..db266eab0e 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -77,7 +77,6 @@ import { } from "./utils"; import { normalizeProtocolRelativeUrl, - normalizeRelativeUrl, PROTOCOL_RELATIVE_URL_REGEX, } from "./url"; @@ -6909,8 +6908,6 @@ function normalizeRedirectLocation( basename: string, historyInstance: History, ): string { - location = normalizeRelativeUrl(location); - if (isAbsoluteUrl(location)) { // Strip off the protocol+origin for same-origin + same-basename absolute redirects let normalizedLocation = location; diff --git a/packages/react-router/lib/router/url.ts b/packages/react-router/lib/router/url.ts index 7c2e61cf9c..078b92a292 100644 --- a/packages/react-router/lib/router/url.ts +++ b/packages/react-router/lib/router/url.ts @@ -1,25 +1,6 @@ export const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i; export const PROTOCOL_RELATIVE_URL_REGEX = /^[\\/]{2}/; -// Normalize characters ignored by the URL parser before determining whether a -// URL is relative or absolute. -export function normalizeRelativeUrl(url: string): string { - if (ABSOLUTE_URL_REGEX.test(url)) { - return url; - } - - let normalized = url.replace(/[\t\n\r]/g, ""); - if (!ABSOLUTE_URL_REGEX.test(normalized)) { - return normalized; - } - - if (PROTOCOL_RELATIVE_URL_REGEX.test(normalized)) { - return normalized.replace(/^[\\/]+/, "/"); - } - - return normalized.replace(/^([a-z][a-z0-9+.-]*):/i, "$1%3A"); -} - export function normalizeProtocolRelativeUrl(url: string, protocol: string) { return protocol + url.replace(/\\/g, "/"); } diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index 338394fe0d..fdc6c62c9b 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -5,7 +5,6 @@ import { invariant, parsePath, warning } from "./history"; import { ABSOLUTE_URL_REGEX, normalizeProtocolRelativeUrl, - normalizeRelativeUrl, PROTOCOL_RELATIVE_URL_REGEX, } from "./url"; @@ -1854,8 +1853,7 @@ export function prependBasename({ return pathname === "/" ? basename : joinPaths([basename, pathname]); } -export const isAbsoluteUrl = (url: string) => - ABSOLUTE_URL_REGEX.test(normalizeRelativeUrl(url)); +export const isAbsoluteUrl = (url: string) => ABSOLUTE_URL_REGEX.test(url); /** * Returns a resolved {@link Path} object relative to the given pathname. @@ -1876,7 +1874,6 @@ export function resolvePath(to: To, fromPathname = "/"): Path { let pathname: string; if (toPathname) { - toPathname = normalizeRelativeUrl(toPathname); toPathname = removeDoubleSlashes(toPathname); if (toPathname.startsWith("/")) { pathname = resolvePathname(toPathname.substring(1), "/"); @@ -2410,9 +2407,7 @@ export function parseToInfo( _to: T, basename: string, ): ParsedLocationInfo { - let to = ( - typeof _to === "string" ? normalizeRelativeUrl(_to) : _to - ) as string; + let to = _to as string; if (typeof to !== "string" || !ABSOLUTE_URL_REGEX.test(to)) { return { absoluteURL: undefined, diff --git a/packages/react-router/lib/rsc/browser.tsx b/packages/react-router/lib/rsc/browser.tsx index 0a38484a5e..0daef296f5 100644 --- a/packages/react-router/lib/rsc/browser.tsx +++ b/packages/react-router/lib/rsc/browser.tsx @@ -25,10 +25,7 @@ import type { RouterContextProvider, } from "../router/utils"; import { ErrorResponseImpl, createContext, resolvePath } from "../router/utils"; -import { - normalizeRelativeUrl, - PROTOCOL_RELATIVE_URL_REGEX, -} from "../router/url"; +import { PROTOCOL_RELATIVE_URL_REGEX } from "../router/url"; import type { DecodedSingleFetchResults, FetchAndDecodeFunction, @@ -1134,8 +1131,6 @@ function isExternalLocation(location: string) { } function normalizeRedirectLocation(location: string): string { - location = normalizeRelativeUrl(location); - if (PROTOCOL_RELATIVE_URL_REGEX.test(location)) { let path = resolvePath(location); return path.pathname + path.search + path.hash;