Skip to content

Commit 8233930

Browse files
authored
fix: handle navigation when browser storage is blocked (#14335)
1 parent b6b8a79 commit 8233930

File tree

3 files changed

+143
-14
lines changed

3 files changed

+143
-14
lines changed

.changeset/yellow-ears-begin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Fail gracefully on manifest version mismatch logic if `sessionStorage` access is blocked
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { test, expect } from "@playwright/test";
2+
import {
3+
createAppFixture,
4+
createFixture,
5+
js,
6+
type AppFixture,
7+
type Fixture,
8+
} from "./helpers/create-fixture.js";
9+
import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
10+
11+
test.describe("sessionStorage denied", () => {
12+
let fixture: Fixture;
13+
let appFixture: AppFixture;
14+
15+
test.beforeAll(async () => {
16+
fixture = await createFixture({
17+
files: {
18+
"app/root.tsx": js`
19+
import * as React from "react";
20+
import { Link, Links, Meta, Outlet, Scripts, useRouteError } from "react-router";
21+
22+
export function ErrorBoundary() {
23+
const error = useRouteError();
24+
console.error("ErrorBoundary caught:", error);
25+
return (
26+
<html>
27+
<head>
28+
<title>Error</title>
29+
<Meta />
30+
<Links />
31+
</head>
32+
<body>
33+
<h1>Application Error</h1>
34+
<pre>{error?.message || "Unknown error"}</pre>
35+
<Scripts />
36+
</body>
37+
</html>
38+
);
39+
}
40+
41+
export default function Root() {
42+
return (
43+
<html lang="en">
44+
<head>
45+
<Meta />
46+
<Links />
47+
</head>
48+
<body>
49+
<nav>
50+
<Link to="/">Home</Link>{" | "}
51+
<Link to="/docs">Docs</Link>
52+
</nav>
53+
<Outlet />
54+
<Scripts />
55+
</body>
56+
</html>
57+
);
58+
}
59+
`,
60+
"app/routes/_index.tsx": js`
61+
export default function Index() {
62+
return <h1>Home</h1>;
63+
}
64+
`,
65+
"app/routes/docs.tsx": js`
66+
export default function Docs() {
67+
return <h1>Documentation</h1>;
68+
}
69+
`,
70+
},
71+
});
72+
appFixture = await createAppFixture(fixture);
73+
});
74+
75+
test("should handle navigation gracefully when storage is blocked", async ({
76+
page,
77+
context,
78+
}) => {
79+
await context.addInitScript(() => {
80+
const storageError = new DOMException(
81+
"Failed to read the 'sessionStorage' property from 'Window': Access is denied for this document.",
82+
"SecurityError",
83+
);
84+
85+
["sessionStorage", "localStorage"].forEach((storage) => {
86+
Object.defineProperty(window, storage, {
87+
get() {
88+
throw storageError;
89+
},
90+
set() {
91+
throw storageError;
92+
},
93+
configurable: false,
94+
});
95+
});
96+
});
97+
98+
let app = new PlaywrightFixture(appFixture, page);
99+
100+
await app.goto("/");
101+
await expect(page.locator("h1")).toContainText("Home");
102+
103+
await app.clickLink("/docs");
104+
await expect(page).toHaveURL(/\/docs$/);
105+
await expect(page.locator("h1")).toContainText("Documentation");
106+
107+
await page.goBack();
108+
await expect(page).toHaveURL(/\/$/);
109+
await expect(page.locator("h1")).toContainText("Home");
110+
111+
await page.goForward();
112+
await expect(page).toHaveURL(/\/docs$/);
113+
await expect(page.locator("h1")).toContainText("Documentation");
114+
});
115+
});

packages/react-router/lib/dom/ssr/fog-of-war.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -263,21 +263,26 @@ export async function fetchAndApplyManifestPatches(
263263
return;
264264
}
265265

266-
// This will hard reload the destination path on navigations, or the
267-
// current path on fetcher calls
268-
if (
269-
sessionStorage.getItem(MANIFEST_VERSION_STORAGE_KEY) ===
270-
manifest.version
271-
) {
272-
// We've already tried fixing for this version, don' try again to
273-
// avoid loops - just let this navigation/fetch 404
274-
console.error(
275-
"Unable to discover routes due to manifest version mismatch.",
276-
);
277-
return;
266+
try {
267+
// This will hard reload the destination path on navigations, or the
268+
// current path on fetcher calls
269+
if (
270+
sessionStorage.getItem(MANIFEST_VERSION_STORAGE_KEY) ===
271+
manifest.version
272+
) {
273+
// We've already tried fixing for this version, don' try again to
274+
// avoid loops - just let this navigation/fetch 404
275+
console.error(
276+
"Unable to discover routes due to manifest version mismatch.",
277+
);
278+
return;
279+
}
280+
281+
sessionStorage.setItem(MANIFEST_VERSION_STORAGE_KEY, manifest.version);
282+
} catch {
283+
// Session storage unavailable
278284
}
279285

280-
sessionStorage.setItem(MANIFEST_VERSION_STORAGE_KEY, manifest.version);
281286
window.location.href = errorReloadPath;
282287
console.warn("Detected manifest version mismatch, reloading...");
283288

@@ -291,7 +296,11 @@ export async function fetchAndApplyManifestPatches(
291296
}
292297

293298
// Reset loop-detection on a successful response
294-
sessionStorage.removeItem(MANIFEST_VERSION_STORAGE_KEY);
299+
try {
300+
sessionStorage.removeItem(MANIFEST_VERSION_STORAGE_KEY);
301+
} catch {
302+
// Session storage unavailable
303+
}
295304
serverPatches = (await res.json()) as AssetsManifest["routes"];
296305
} catch (e) {
297306
if (signal?.aborted) return;

0 commit comments

Comments
 (0)