Skip to content

Commit

Permalink
throw on non-serializable state PUSH navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed May 1, 2023
1 parent fdb9069 commit 7fe2db3
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .changeset/non-serializable-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@remix-run/router": patch
"react-router-dom": patch
---

Re-throw `DOMException` (`DataCloneError`) when attempting to perform a `PUSH` navigation with non-serializable state.
19 changes: 19 additions & 0 deletions packages/router/__tests__/TestSequences/PushStateInvalid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { DOMWindow } from "jsdom";
import type { History } from "../../history";

export default function PushState(history: History, window: DOMWindow) {
let err = new DOMException("ERROR", "DataCloneError");
jest.spyOn(window.history, "pushState").mockImplementation(() => {
throw err;
});

expect(history.location).toMatchObject({
pathname: "/",
});

expect(() =>
history.push("/home?the=query#the-hash", { invalid: () => {} })
).toThrow(err);

expect(history.location.pathname).toBe("/");
}
8 changes: 7 additions & 1 deletion packages/router/__tests__/browser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Listen from "./TestSequences/Listen";
import PushNewLocation from "./TestSequences/PushNewLocation";
import PushSamePath from "./TestSequences/PushSamePath";
import PushState from "./TestSequences/PushState";
import PushStateInvalid from "./TestSequences/PushStateInvalid";
import PushMissingPathname from "./TestSequences/PushMissingPathname";
import PushRelativePathname from "./TestSequences/PushRelativePathname";
import ReplaceNewLocation from "./TestSequences/ReplaceNewLocation";
Expand All @@ -22,10 +23,11 @@ import ListenPopOnly from "./TestSequences/ListenPopOnly";

describe("a browser history", () => {
let history: BrowserHistory;
let dom: JSDOM;

beforeEach(() => {
// Need to use our own custom DOM in order to get a working history
const dom = new JSDOM(`<!DOCTYPE html><p>History Example</p>`, {
dom = new JSDOM(`<!DOCTYPE html><p>History Example</p>`, {
url: "https://example.org/",
});
dom.window.history.replaceState(null, "", "/");
Expand Down Expand Up @@ -91,6 +93,10 @@ describe("a browser history", () => {
it("calls change listeners with the new location", () => {
PushState(history);
});

it("re-throws when using non-serializable state", () => {
PushStateInvalid(history, dom.window);
});
});

describe("push with no pathname", () => {
Expand Down
8 changes: 7 additions & 1 deletion packages/router/__tests__/hash-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import InitialLocationDefaultKey from "./TestSequences/InitialLocationDefaultKey
import PushNewLocation from "./TestSequences/PushNewLocation";
import PushSamePath from "./TestSequences/PushSamePath";
import PushState from "./TestSequences/PushState";
import PushStateInvalid from "./TestSequences/PushStateInvalid";
import PushMissingPathname from "./TestSequences/PushMissingPathname";
import PushRelativePathnameWarning from "./TestSequences/PushRelativePathnameWarning";
import ReplaceNewLocation from "./TestSequences/ReplaceNewLocation";
Expand All @@ -26,10 +27,11 @@ import ListenPopOnly from "./TestSequences/ListenPopOnly";

describe("a hash history", () => {
let history: HashHistory;
let dom: JSDOM;

beforeEach(() => {
// Need to use our own custom DOM in order to get a working history
const dom = new JSDOM(`<!DOCTYPE html><p>History Example</p>`, {
dom = new JSDOM(`<!DOCTYPE html><p>History Example</p>`, {
url: "https://example.org/",
});
dom.window.history.replaceState(null, "", "#/");
Expand Down Expand Up @@ -95,6 +97,10 @@ describe("a hash history", () => {
it("calls change listeners with the new location", () => {
PushState(history);
});

it("re-throws when using non-serializable state", () => {
PushStateInvalid(history, dom.window);
});
});

describe("push with no pathname", () => {
Expand Down
7 changes: 7 additions & 0 deletions packages/router/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,13 @@ function getUrlBasedHistory(
try {
globalHistory.pushState(historyState, "", url);
} catch (error) {
// If the exception is because `state` can't be serialized, let that throw
// outwards just like a replace call would so the dev knows the cause
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps
// https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
if (error instanceof DOMException && error.name === "DataCloneError") {
throw error;
}
// They are going to lose state here, but there is no real
// way to warn them about it since the page will refresh...
window.location.assign(url);
Expand Down

0 comments on commit 7fe2db3

Please sign in to comment.