Skip to content
Open
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
115 changes: 115 additions & 0 deletions integration/session-storage-denied-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { test, expect } from "@playwright/test";
import {
createAppFixture,
createFixture,
js,
type AppFixture,
type Fixture,
} from "./helpers/create-fixture.js";
import { PlaywrightFixture } from "./helpers/playwright-fixture.js";

test.describe("sessionStorage denied", () => {
let fixture: Fixture;
let appFixture: AppFixture;

test.beforeAll(async () => {
fixture = await createFixture({
files: {
"app/root.tsx": js`
import * as React from "react";
import { Link, Links, Meta, Outlet, Scripts, useRouteError } from "react-router";

export function ErrorBoundary() {
const error = useRouteError();
console.error("ErrorBoundary caught:", error);
return (
<html>
<head>
<title>Error</title>
<Meta />
<Links />
</head>
<body>
<h1>Application Error</h1>
<pre>{error?.message || "Unknown error"}</pre>
<Scripts />
</body>
</html>
);
}

export default function Root() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<nav>
<Link to="/">Home</Link>{" | "}
<Link to="/docs">Docs</Link>
</nav>
<Outlet />
<Scripts />
</body>
</html>
);
}
`,
"app/routes/_index.tsx": js`
export default function Index() {
return <h1>Home</h1>;
}
`,
"app/routes/docs.tsx": js`
export default function Docs() {
return <h1>Documentation</h1>;
}
`,
},
});
appFixture = await createAppFixture(fixture);
});

test("should handle navigation gracefully when storage is blocked", async ({
page,
context,
}) => {
await context.addInitScript(() => {
const storageError = new DOMException(
"Failed to read the 'sessionStorage' property from 'Window': Access is denied for this document.",
"SecurityError",
);

["sessionStorage", "localStorage"].forEach((storage) => {
Object.defineProperty(window, storage, {
get() {
throw storageError;
},
set() {
throw storageError;
},
configurable: false,
});
});
});

let app = new PlaywrightFixture(appFixture, page);

await app.goto("/");
await expect(page.locator("h1")).toContainText("Home");

await app.clickLink("/docs");
await expect(page).toHaveURL(/\/docs$/);
await expect(page.locator("h1")).toContainText("Documentation");

await page.goBack();
await expect(page).toHaveURL(/\/$/);
await expect(page.locator("h1")).toContainText("Home");

await page.goForward();
await expect(page).toHaveURL(/\/docs$/);
await expect(page.locator("h1")).toContainText("Documentation");
});
});
39 changes: 25 additions & 14 deletions packages/react-router/lib/dom/ssr/fog-of-war.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,21 +263,27 @@ export async function fetchAndApplyManifestPatches(
return;
}

// This will hard reload the destination path on navigations, or the
// current path on fetcher calls
if (
sessionStorage.getItem(MANIFEST_VERSION_STORAGE_KEY) ===
manifest.version
) {
// We've already tried fixing for this version, don' try again to
// avoid loops - just let this navigation/fetch 404
console.error(
"Unable to discover routes due to manifest version mismatch.",
);
return;
try {
// This will hard reload the destination path on navigations, or the
// current path on fetcher calls
if (
sessionStorage.getItem(MANIFEST_VERSION_STORAGE_KEY) ===
manifest.version
) {
// We've already tried fixing for this version, don' try again to
// avoid loops - just let this navigation/fetch 404
console.error(
"Unable to discover routes due to manifest version mismatch.",
);
return;
}

sessionStorage.setItem(MANIFEST_VERSION_STORAGE_KEY, manifest.version);
} catch (error) {
// Catches when storage is blocked
// Skip version tracking and continue with reload
}

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

Expand All @@ -291,7 +297,12 @@ export async function fetchAndApplyManifestPatches(
}

// Reset loop-detection on a successful response
sessionStorage.removeItem(MANIFEST_VERSION_STORAGE_KEY);
try {
sessionStorage.removeItem(MANIFEST_VERSION_STORAGE_KEY);
} catch {
// Catches when storage is blocked
// Skip version tracking and continue with reload
}
serverPatches = (await res.json()) as AssetsManifest["routes"];
} catch (e) {
if (signal?.aborted) return;
Expand Down
Loading