Skip to content

Commit 06f712f

Browse files
authored
throw on non-serializable state PUSH navigation (#10427)
1 parent 6dac8de commit 06f712f

File tree

5 files changed

+46
-2
lines changed

5 files changed

+46
-2
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@remix-run/router": patch
3+
"react-router-dom": patch
4+
---
5+
6+
Re-throw `DOMException` (`DataCloneError`) when attempting to perform a `PUSH` navigation with non-serializable state.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { DOMWindow } from "jsdom";
2+
import type { History } from "../../history";
3+
4+
export default function PushState(history: History, window: DOMWindow) {
5+
let err = new DOMException("ERROR", "DataCloneError");
6+
jest.spyOn(window.history, "pushState").mockImplementation(() => {
7+
throw err;
8+
});
9+
10+
expect(history.location).toMatchObject({
11+
pathname: "/",
12+
});
13+
14+
expect(() =>
15+
history.push("/home?the=query#the-hash", { invalid: () => {} })
16+
).toThrow(err);
17+
18+
expect(history.location.pathname).toBe("/");
19+
}

packages/router/__tests__/browser-test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Listen from "./TestSequences/Listen";
1010
import PushNewLocation from "./TestSequences/PushNewLocation";
1111
import PushSamePath from "./TestSequences/PushSamePath";
1212
import PushState from "./TestSequences/PushState";
13+
import PushStateInvalid from "./TestSequences/PushStateInvalid";
1314
import PushMissingPathname from "./TestSequences/PushMissingPathname";
1415
import PushRelativePathname from "./TestSequences/PushRelativePathname";
1516
import ReplaceNewLocation from "./TestSequences/ReplaceNewLocation";
@@ -22,10 +23,11 @@ import ListenPopOnly from "./TestSequences/ListenPopOnly";
2223

2324
describe("a browser history", () => {
2425
let history: BrowserHistory;
26+
let dom: JSDOM;
2527

2628
beforeEach(() => {
2729
// Need to use our own custom DOM in order to get a working history
28-
const dom = new JSDOM(`<!DOCTYPE html><p>History Example</p>`, {
30+
dom = new JSDOM(`<!DOCTYPE html><p>History Example</p>`, {
2931
url: "https://example.org/",
3032
});
3133
dom.window.history.replaceState(null, "", "/");
@@ -91,6 +93,10 @@ describe("a browser history", () => {
9193
it("calls change listeners with the new location", () => {
9294
PushState(history);
9395
});
96+
97+
it("re-throws when using non-serializable state", () => {
98+
PushStateInvalid(history, dom.window);
99+
});
94100
});
95101

96102
describe("push with no pathname", () => {

packages/router/__tests__/hash-test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import InitialLocationDefaultKey from "./TestSequences/InitialLocationDefaultKey
1010
import PushNewLocation from "./TestSequences/PushNewLocation";
1111
import PushSamePath from "./TestSequences/PushSamePath";
1212
import PushState from "./TestSequences/PushState";
13+
import PushStateInvalid from "./TestSequences/PushStateInvalid";
1314
import PushMissingPathname from "./TestSequences/PushMissingPathname";
1415
import PushRelativePathnameWarning from "./TestSequences/PushRelativePathnameWarning";
1516
import ReplaceNewLocation from "./TestSequences/ReplaceNewLocation";
@@ -26,10 +27,11 @@ import ListenPopOnly from "./TestSequences/ListenPopOnly";
2627

2728
describe("a hash history", () => {
2829
let history: HashHistory;
30+
let dom: JSDOM;
2931

3032
beforeEach(() => {
3133
// Need to use our own custom DOM in order to get a working history
32-
const dom = new JSDOM(`<!DOCTYPE html><p>History Example</p>`, {
34+
dom = new JSDOM(`<!DOCTYPE html><p>History Example</p>`, {
3335
url: "https://example.org/",
3436
});
3537
dom.window.history.replaceState(null, "", "#/");
@@ -95,6 +97,10 @@ describe("a hash history", () => {
9597
it("calls change listeners with the new location", () => {
9698
PushState(history);
9799
});
100+
101+
it("re-throws when using non-serializable state", () => {
102+
PushStateInvalid(history, dom.window);
103+
});
98104
});
99105

100106
describe("push with no pathname", () => {

packages/router/history.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,13 @@ function getUrlBasedHistory(
634634
try {
635635
globalHistory.pushState(historyState, "", url);
636636
} catch (error) {
637+
// If the exception is because `state` can't be serialized, let that throw
638+
// outwards just like a replace call would so the dev knows the cause
639+
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps
640+
// https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
641+
if (error instanceof DOMException && error.name === "DataCloneError") {
642+
throw error;
643+
}
637644
// They are going to lose state here, but there is no real
638645
// way to warn them about it since the page will refresh...
639646
window.location.assign(url);

0 commit comments

Comments
 (0)