Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
28 changes: 8 additions & 20 deletions integration/error-sanitization-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,8 @@ test.describe("Error Sanitization", () => {
expect(html).toMatch("Index Error");
expect(html).not.toMatch("LOADER");
expect(html).toMatch("MESSAGE:Unexpected Server Error");
// This is the turbo-stream encoding - the fact that stack goes right
// into __type means it has no value
expect(html).toMatch(
'\\"message\\",\\"Unexpected Server Error\\",\\"stack\\",\\"__type\\",\\"Error\\"',
);
expect(html).toMatch('\\"SanitizedError\\"');
expect(html).toMatch('\\"Error\\",\\"Unexpected Server Error\\"');
expect(html).not.toMatch(/ at /i);
expect(errorLogs.length).toBe(1);
expect(errorLogs[0][0].message).toMatch("Loader Error");
Expand All @@ -195,11 +192,8 @@ test.describe("Error Sanitization", () => {
let html = await response.text();
expect(html).toMatch("Index Error");
expect(html).toMatch("MESSAGE:Unexpected Server Error");
// This is the turbo-stream encoding - the fact that stack goes right
// into __type means it has no value
expect(html).toMatch(
'\\"message\\",\\"Unexpected Server Error\\",\\"stack\\",\\"__type\\",\\"Error\\"',
);
expect(html).toMatch('\\"SanitizedError\\"');
expect(html).toMatch('\\"Error\\",\\"Unexpected Server Error\\"');
expect(html).not.toMatch(/ at /i);
expect(errorLogs.length).toBe(1);
expect(errorLogs[0][0].message).toMatch("Render Error");
Expand Down Expand Up @@ -571,11 +565,8 @@ test.describe("Error Sanitization", () => {
expect(html).toMatch("Index Error");
expect(html).not.toMatch("LOADER");
expect(html).toMatch("MESSAGE:Unexpected Server Error");
// This is the turbo-stream encoding - the fact that stack goes right
// into __type means it has no value
expect(html).toMatch(
'\\"message\\",\\"Unexpected Server Error\\",\\"stack\\",\\"__type\\",\\"Error\\"',
);
expect(html).toMatch('\\"SanitizedError\\"');
expect(html).toMatch('\\"Error\\",\\"Unexpected Server Error\\"');
expect(html).not.toMatch(/ at /i);
expect(errorLogs[0][0]).toEqual("App Specific Error Logging:");
expect(errorLogs[1][0]).toEqual(" Request: GET test://test/?loader");
Expand All @@ -589,11 +580,8 @@ test.describe("Error Sanitization", () => {
let html = await response.text();
expect(html).toMatch("Index Error");
expect(html).toMatch("MESSAGE:Unexpected Server Error");
// This is the turbo-stream encoding - the fact that stack goes right
// into __type means it has no value
expect(html).toMatch(
'\\"message\\",\\"Unexpected Server Error\\",\\"stack\\",\\"__type\\",\\"Error\\"',
);
expect(html).toMatch('\\"SanitizedError\\"');
expect(html).toMatch('\\"Error\\",\\"Unexpected Server Error\\"');
expect(html).not.toMatch(/ at /i);
expect(errorLogs[0][0]).toEqual("App Specific Error Logging:");
expect(errorLogs[1][0]).toEqual(" Request: GET test://test/?render");
Expand Down
31 changes: 31 additions & 0 deletions integration/rsc/rsc-nojs-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,16 @@ implementations.forEach((implementation) => {
throw redirect("https://example.com/");
}

if (id === "unsupported-protocol") {
throw redirect("about:blank");
}

return (
<>
<h1>{id || "home"}</h1>
<Link to="/render-redirect/redirect">Redirect</Link>
<Link to="/render-redirect/external">External</Link>
<Link to="/render-redirect/unsupported-protocol">Unsupported</Link>
</>
)
}
Expand Down Expand Up @@ -147,11 +152,16 @@ implementations.forEach((implementation) => {
throw redirect("https://example.com/");
}

if (id === "unsupported-protocol") {
throw redirect("about:blank");
}

return (
<>
<h1>{id || "home"}</h1>
<Link to="/render-redirect/lazy/redirect">Redirect</Link>
<Link to="/render-redirect/external">External</Link>
<Link to="/render-redirect/lazy/unsupported-protocol">Unsupported</Link>
</>
);
}
Expand Down Expand Up @@ -234,6 +244,16 @@ implementations.forEach((implementation) => {
await expect(page.getByText("Example Domain")).toBeAttached();
});

test("Handles unsupported protocol redirect Responses from render", async ({
page,
}) => {
let response = await page.request.get(
`http://localhost:${port}/render-redirect/unsupported-protocol`,
{ maxRedirects: 0 },
);
expect(response.headers()["location"]).not.toBe("about:blank");
});

test("Suppport throwing redirect Response from suspended render", async ({
page,
}) => {
Expand All @@ -256,5 +276,16 @@ implementations.forEach((implementation) => {
await page.waitForURL(`https://example.com/`);
await expect(page.getByText("Example Domain")).toBeAttached();
});

test("Handles unsupported protocol redirect Responses from suspended render", async ({
page,
}) => {
let response = await page.request.get(
`http://localhost:${port}/render-redirect/lazy/unsupported-protocol`,
);
expect(await response.text()).not.toContain(
'<meta http-equiv="refresh" content="0;url=about:',
);
});
});
});
34 changes: 34 additions & 0 deletions integration/rsc/rsc-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1490,11 +1490,16 @@ implementations.forEach((implementation) => {
throw redirect("https://example.com/")
}

if (id === "unsupported-protocol") {
throw redirect("about:blank")
}

return (
<>
<h1>{id || "home"}</h1>
<Link to="/render-redirect/redirect">Redirect</Link>
<Link to="/render-redirect/external">External</Link>
<Link to="/render-redirect/unsupported-protocol">Unsupported</Link>
</>
)
}
Expand Down Expand Up @@ -1522,11 +1527,16 @@ implementations.forEach((implementation) => {
throw redirect("https://example.com/")
}

if (id === "unsupported-protocol") {
throw redirect("about:blank")
}

return (
<>
<h1>{id || "home"}</h1>
<Link to="/render-redirect/lazy/redirect">Redirect</Link>
<Link to="/render-redirect/external">External</Link>
<Link to="/render-redirect/lazy/unsupported-protocol">Unsupported</Link>
</>
);
}
Expand Down Expand Up @@ -1867,6 +1877,18 @@ implementations.forEach((implementation) => {
await expect(page.getByText("Example Domain")).toBeAttached();
});

test("Handles unsupported protocol redirect Responses from render", async ({
page,
}) => {
await page.goto(`http://localhost:${port}/render-redirect`);
await expect(page.getByText("home")).toBeAttached();
await page.getByText("Unsupported").click();
await page.waitForTimeout(500);
await expect(page).toHaveURL(
`http://localhost:${port}/render-redirect/unsupported-protocol`,
);
});

test("Suppport throwing redirect Response from suspended render", async ({
page,
}) => {
Expand Down Expand Up @@ -1894,6 +1916,18 @@ implementations.forEach((implementation) => {
await expect(page.getByText("Example Domain")).toBeAttached();
});

test("Handles unsupported protocol redirect Responses from suspended render", async ({
page,
}) => {
await page.goto(`http://localhost:${port}/render-redirect/lazy`);
await expect(page.getByText("home")).toBeAttached();
await page.getByText("Unsupported").click();
await page.waitForTimeout(500);
await expect(page).toHaveURL(
`http://localhost:${port}/render-redirect/lazy/unsupported-protocol`,
);
});

test("Support throwing Responses", async ({ page }) => {
await page.goto(
`http://localhost:${port}/render-route-error-response`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use `turbo-stream` to serialize and deserialize Framework Mode hydration errors
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Remove the un-documented custom error serialization logic from Data Mode SSR built-in hydration flows
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Validate protocols in RSC render redirects
1 change: 1 addition & 0 deletions packages/react-router/.changes/patch.url-consolidation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Consolidate url normalization logic and better handle mixed slashes
50 changes: 50 additions & 0 deletions packages/react-router/__tests__/dom/data-browser-router-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8103,6 +8103,56 @@ function testDomRouter(
`);
});

it("does not deserialize custom Error subclass instances from the window", () => {
try {
(window as any).CustomError = class CustomError extends Error {};
window.__staticRouterHydrationData = {
loaderData: {},
actionData: null,
errors: {
"0": {
message: "custom error message",
__type: "Error",
__subType: "CustomError",
},
},
};
let router = createTestRouter([
{
path: "/",
Component: () => <h1>Nope</h1>,
ErrorBoundary: () => <Boundary />,
},
]);
let { container } = render(<RouterProvider router={router} />);

function Boundary() {
let error = useRouteError() as Error;
return error instanceof Error ? (
<>
<pre>{error.constructor.name}</pre>
<pre>{error.toString()}</pre>
</>
) : (
<p>No :(</p>
);
}

expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<pre>
Error
</pre>
<pre>
Error: custom error message
</pre>
</div>"
`);
} finally {
delete (window as any).CustomError;
}
});

it("renders hydration errors on leaf elements", async () => {
let router = createTestRouter(
[
Expand Down
16 changes: 16 additions & 0 deletions packages/react-router/__tests__/resolvePath-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ describe("resolvePath", () => {
pathname: "/foo",
});

expect(resolvePath("//foo")).toMatchObject({
pathname: "/foo",
});

expect(resolvePath("\\\\foo")).toMatchObject({
pathname: "/foo",
});

expect(resolvePath("/\\foo")).toMatchObject({
pathname: "/foo",
});

expect(resolvePath("\\/foo")).toMatchObject({
pathname: "/foo",
});

spy.mockRestore();
});

Expand Down
34 changes: 34 additions & 0 deletions packages/react-router/__tests__/router/redirects-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,40 @@ describe("redirects", () => {
});
});

it("normalizes mixed leading separators in redirects", async () => {
let locations = [
"\\\\localhost/parent",
"/\\localhost/parent",
"\\/localhost/parent",
];

for (let location of locations) {
let t = setup({ routes: REDIRECT_ROUTES });

let A = await t.navigate("/parent/child", {
formMethod: "post",
formData: createFormData({}),
});

let B = await A.actions.child.redirectReturn(
location,
undefined,
undefined,
["parent"],
);
await B.loaders.parent.resolve("PARENT");
expect(t.router.state.location).toMatchObject({
hash: "",
pathname: "/parent",
search: "",
state: {
_isRedirect: true,
},
});
expect(t.window.location.assign).not.toHaveBeenCalled();
}
});

it("properly handles same-origin absolute URLs when using a basename", async () => {
let t = setup({ routes: REDIRECT_ROUTES, basename: "/base" });

Expand Down
30 changes: 30 additions & 0 deletions packages/react-router/__tests__/server-runtime/data-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { decodeViaTurboStream } from "../../lib/dom/ssr/single-fetch";
import {
ErrorResponseImpl,
isRouteErrorResponse,
} from "../../lib/router/utils";
import { createRequestHandler } from "../../lib/server-runtime/server";
import { encodeViaTurboStream } from "../../lib/server-runtime/single-fetch";
import { ServerMode } from "../../lib/server-runtime/mode";
import { mockServerBuild } from "./utils";

describe("loaders", () => {
Expand Down Expand Up @@ -33,3 +39,27 @@ describe("loaders", () => {
expect((decoded.value as any)[routeId].data).toEqual("/random");
});
});

describe("turbo-stream error decoding", () => {
it("decodes ErrorResponse instances", async () => {
let body = encodeViaTurboStream(
{
errors: {
root: new ErrorResponseImpl(404, "Not Found", "Missing", true),
},
},
new AbortController().signal,
undefined,
ServerMode.Development,
);

let decoded = await decodeViaTurboStream(body, global);
let error = (decoded.value as any).errors.root;

expect(isRouteErrorResponse(error)).toBe(true);
expect(error.status).toBe(404);
expect(error.statusText).toBe("Not Found");
expect(error.data).toBe("Missing");
expect(error.internal).toBe(false);
});
});
27 changes: 27 additions & 0 deletions packages/react-router/__tests__/useNavigate-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,33 @@ describe("useNavigate", () => {
</h1>
`);
});

it("normalizes mixed leading separators", async () => {
for (let to of ["//foo", "\\\\foo", "/\\foo", "\\/foo"]) {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/home"]}>
<Routes>
<Route path="home" element={<UseNavigateButton to={to} />} />
<Route path="foo" element={<p>foo</p>} />
</Routes>
</MemoryRouter>,
);
});

// @ts-expect-error
let button = renderer.root.findByType("button");
await TestRenderer.act(() => button.props.onClick());

// @ts-expect-error
expect(renderer.toJSON()).toMatchInlineSnapshot(`
<p>
foo
</p>
`);
}
});
});

describe("with a relative href (relative=route)", () => {
Expand Down
Loading