diff --git a/.changeset/dirty-yaks-repair.md b/.changeset/dirty-yaks-repair.md new file mode 100644 index 0000000000..b2896efa27 --- /dev/null +++ b/.changeset/dirty-yaks-repair.md @@ -0,0 +1,6 @@ +--- +"react-router": patch +"@remix-run/router": patch +--- + +fix: throw error when receiving invalid path object (#9375) diff --git a/packages/react-router/__tests__/useNavigate-test.tsx b/packages/react-router/__tests__/useNavigate-test.tsx index e51313c881..6e0dc55006 100644 --- a/packages/react-router/__tests__/useNavigate-test.tsx +++ b/packages/react-router/__tests__/useNavigate-test.tsx @@ -164,6 +164,82 @@ describe("useNavigate", () => { `); }); + it("throws on invalid destination path objects", () => { + function Home() { + let navigate = useNavigate(); + + return ( +
+

Home

+ + + + +
+ ); + } + + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + } /> + + + ); + }); + + expect(() => + TestRenderer.act(() => { + renderer.root.findAllByType("button")[0].props.onClick(); + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Cannot include a '?' character in a manually specified \`to.pathname\` field [{\\"pathname\\":\\"/about/thing?search\\"}]. Please separate it out to the \`to.search\` field. Alternatively you may provide the full path as a string in and the router will parse it for you."` + ); + + expect(() => + TestRenderer.act(() => { + renderer.root.findAllByType("button")[1].props.onClick(); + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Cannot include a '#' character in a manually specified \`to.pathname\` field [{\\"pathname\\":\\"/about/thing#hash\\"}]. Please separate it out to the \`to.hash\` field. Alternatively you may provide the full path as a string in and the router will parse it for you."` + ); + + expect(() => + TestRenderer.act(() => { + renderer.root.findAllByType("button")[2].props.onClick(); + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Cannot include a '?' character in a manually specified \`to.pathname\` field [{\\"pathname\\":\\"/about/thing?search#hash\\"}]. Please separate it out to the \`to.search\` field. Alternatively you may provide the full path as a string in and the router will parse it for you."` + ); + + expect(() => + TestRenderer.act(() => { + renderer.root.findAllByType("button")[3].props.onClick(); + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Cannot include a '#' character in a manually specified \`to.search\` field [{\\"pathname\\":\\"/about/thing\\",\\"search\\":\\"?search#hash\\"}]. Please separate it out to the \`to.hash\` field. Alternatively you may provide the full path as a string in and the router will parse it for you."` + ); + }); + describe("with state", () => { it("adds the state to location.state", () => { function Home() { diff --git a/packages/router/utils.ts b/packages/router/utils.ts index cb3e89b85d..9b593cd1bf 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -768,6 +768,22 @@ function resolvePathname(relativePath: string, fromPathname: string): string { return segments.length > 1 ? segments.join("/") : "/"; } +function getInvalidPathError( + char: string, + field: string, + dest: string, + path: Partial +) { + return ( + `Cannot include a '${char}' character in a manually specified ` + + `\`to.${field}\` field [${JSON.stringify( + path + )}]. Please separate it out to the ` + + `\`to.${dest}\` field. Alternatively you may provide the full path as ` + + `a string in and the router will parse it for you.` + ); +} + /** * @private */ @@ -777,7 +793,26 @@ export function resolveTo( locationPathname: string, isPathRelative = false ): Path { - let to = typeof toArg === "string" ? parsePath(toArg) : { ...toArg }; + let to: Partial; + if (typeof toArg === "string") { + to = parsePath(toArg); + } else { + to = { ...toArg }; + + invariant( + !to.pathname || !to.pathname.includes("?"), + getInvalidPathError("?", "pathname", "search", to) + ); + invariant( + !to.pathname || !to.pathname.includes("#"), + getInvalidPathError("#", "pathname", "hash", to) + ); + invariant( + !to.search || !to.search.includes("#"), + getInvalidPathError("#", "search", "hash", to) + ); + } + let isEmptyPath = toArg === "" || to.pathname === ""; let toPathname = isEmptyPath ? "/" : to.pathname;