Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/invalid-link-to.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router-dom": patch
---

Fail gracefully on `<Link to="//">` and other invalid URL values
20 changes: 20 additions & 0 deletions packages/react-router-dom/__tests__/link-href-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -907,4 +907,24 @@ describe("<Link> href", () => {
);
});
});

test("fails gracefully on invalid `to` values", () => {
let warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter>
<Routes>
<Route path="/" element={<Link to="//" />} />
</Routes>
</MemoryRouter>
);
});

expect(renderer.root.findByType("a").props.href).toEqual("//");
expect(warnSpy).toHaveBeenCalledWith(
'<Link to="//"> contains an invalid URL which will probably break when clicked - please update to a valid URL path.'
);
warnSpy.mockRestore();
});
});
31 changes: 20 additions & 11 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -449,17 +449,26 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(

// Only check for external origins client-side
if (isBrowser) {
let currentUrl = new URL(window.location.href);
let targetUrl = to.startsWith("//")
? new URL(currentUrl.protocol + to)
: new URL(to);
let path = stripBasename(targetUrl.pathname, basename);

if (targetUrl.origin === currentUrl.origin && path != null) {
// Strip the protocol/origin/basename for same-origin absolute URLs
to = path + targetUrl.search + targetUrl.hash;
} else {
isExternal = true;
try {
let currentUrl = new URL(window.location.href);
let targetUrl = to.startsWith("//")
? new URL(currentUrl.protocol + to)
: new URL(to);
let path = stripBasename(targetUrl.pathname, basename);

if (targetUrl.origin === currentUrl.origin && path != null) {
// Strip the protocol/origin/basename for same-origin absolute URLs
to = path + targetUrl.search + targetUrl.hash;
} else {
isExternal = true;
}
} catch (e) {
// We can't do external URL detection without a valid URL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useful to check whether e instanceof TypeError or rethrow? https://developer.mozilla.org/en-US/docs/Web/API/URL/URL#exceptions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of the goal here is that a broken link is better than a crashed render (see remix-run/remix#5440 (comment)), so I don't think re-throwing is what we want to do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair

warning(
false,
`<Link to="${to}"> contains an invalid URL which will probably break ` +
`when clicked - please update to a valid URL path.`
);
}
}
}
Expand Down