Skip to content

Commit

Permalink
Hotfix: absolute URLs on server router (#10112)
Browse files Browse the repository at this point in the history
Co-authored-by: Matt Brophy <matt@brophy.org>
  • Loading branch information
thomasverleye and brophdawg11 committed Feb 22, 2023
1 parent 3c6fb46 commit 8975de9
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/lovely-cheetahs-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router-dom": patch
---

Fix SSR of absolute Link urls
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
- tanayv
- theostavrides
- thisiskartik
- thomasverleye
- ThornWu
- timdorr
- TkDodo
Expand Down
42 changes: 42 additions & 0 deletions packages/react-router-dom/__tests__/data-static-router-test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* @jest-environment node
*/

import * as React from "react";
import * as ReactDOMServer from "react-dom/server";
import type { StaticHandlerContext } from "@remix-run/router";
Expand Down Expand Up @@ -642,6 +646,44 @@ describe("A <StaticRouterProvider>", () => {
`);
});

it("renders absolute links correctly", async () => {
let routes = [
{
path: "/",
element: (
<>
<Link to="/the/path">relative path</Link>
<Link to="http://localhost/the/path">absolute same-origin url</Link>
<Link to="https://remix.run">absolute different-origin url</Link>
<Link to="mailto:foo@baz.com">absolute mailto: url</Link>
</>
),
},
];
let { query } = createStaticHandler(routes);

let context = (await query(
new Request("http://localhost/", {
signal: new AbortController().signal,
})
)) as StaticHandlerContext;

let html = ReactDOMServer.renderToStaticMarkup(
<React.StrictMode>
<StaticRouterProvider
router={createStaticRouter(routes, context)}
context={context}
/>
</React.StrictMode>
);
expect(html).toMatch(
'<a href="/the/path">relative path</a>' +
'<a href="http://localhost/the/path">absolute same-origin url</a>' +
'<a href="https://remix.run">absolute different-origin url</a>' +
'<a href="mailto:foo@baz.com">absolute mailto: url</a>'
);
});

describe("boundary tracking", () => {
it("tracks the deepest boundary during render", async () => {
let routes = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Polyfill jsdom SubmitEvent.submitter, until https://github.com/jsdom/jsdom/pull/3481 is merged
if (
typeof SubmitEvent === "undefined" ||
!SubmitEvent.prototype.hasOwnProperty("submitter")
typeof window !== "undefined" &&
(typeof SubmitEvent === "undefined" ||
!SubmitEvent.prototype.hasOwnProperty("submitter"))
) {
const setImmediate = (fn, ...args) => global.setTimeout(fn, 0, ...args);

Expand Down
3 changes: 2 additions & 1 deletion packages/react-router-dom/__tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
TextEncoder as NodeTextEncoder,
TextDecoder as NodeTextDecoder,
} from "util";
import { fetch, Request, Response } from "@remix-run/web-fetch";
import { fetch, Request, Response, Headers } from "@remix-run/web-fetch";
import { AbortController as NodeAbortController } from "abort-controller";

import "./polyfills/SubmitEvent.submitter";
Expand All @@ -22,6 +22,7 @@ if (!globalThis.fetch) {
// web-std/fetch Response does not currently implement Response.error()
// @ts-expect-error
globalThis.Response = Response;
globalThis.Headers = Headers;
}

if (!globalThis.AbortController) {
Expand Down
31 changes: 17 additions & 14 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,8 @@ const isBrowser =
typeof window.document !== "undefined" &&
typeof window.document.createElement !== "undefined";

const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;

/**
* The public API for rendering a history-aware <a>.
*/
Expand All @@ -422,21 +424,22 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
let absoluteHref;
let isExternal = false;

if (
isBrowser &&
typeof to === "string" &&
/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(to)
) {
if (typeof to === "string" && ABSOLUTE_URL_REGEX.test(to)) {
// Render the absolute href server- and client-side
absoluteHref = to;
let currentUrl = new URL(window.location.href);
let targetUrl = to.startsWith("//")
? new URL(currentUrl.protocol + to)
: new URL(to);
if (targetUrl.origin === currentUrl.origin) {
// Strip the protocol/origin for same-origin absolute URLs
to = targetUrl.pathname + targetUrl.search + targetUrl.hash;
} else {
isExternal = true;

// 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);
if (targetUrl.origin === currentUrl.origin) {
// Strip the protocol/origin for same-origin absolute URLs
to = targetUrl.pathname + targetUrl.search + targetUrl.hash;
} else {
isExternal = true;
}
}
}

Expand Down

0 comments on commit 8975de9

Please sign in to comment.