diff --git a/.changeset/yellow-ears-begin.md b/.changeset/yellow-ears-begin.md new file mode 100644 index 0000000000..8d0c3c80d8 --- /dev/null +++ b/.changeset/yellow-ears-begin.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Fail gracefully on manifest version mismatch logic if `sessionStorage` access is blocked diff --git a/integration/session-storage-denied-test.ts b/integration/session-storage-denied-test.ts new file mode 100644 index 0000000000..4c18b4e2f9 --- /dev/null +++ b/integration/session-storage-denied-test.ts @@ -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 ( + + + Error + + + + +

Application Error

+
{error?.message || "Unknown error"}
+ + + + ); + } + + export default function Root() { + return ( + + + + + + + + + + + + ); + } + `, + "app/routes/_index.tsx": js` + export default function Index() { + return

Home

; + } + `, + "app/routes/docs.tsx": js` + export default function Docs() { + return

Documentation

; + } + `, + }, + }); + 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"); + }); +}); diff --git a/packages/react-router/lib/dom/ssr/fog-of-war.ts b/packages/react-router/lib/dom/ssr/fog-of-war.ts index 5770fda779..3bfc807517 100644 --- a/packages/react-router/lib/dom/ssr/fog-of-war.ts +++ b/packages/react-router/lib/dom/ssr/fog-of-war.ts @@ -263,21 +263,26 @@ 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 { + // Session storage unavailable } - sessionStorage.setItem(MANIFEST_VERSION_STORAGE_KEY, manifest.version); window.location.href = errorReloadPath; console.warn("Detected manifest version mismatch, reloading..."); @@ -291,7 +296,11 @@ 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 { + // Session storage unavailable + } serverPatches = (await res.json()) as AssetsManifest["routes"]; } catch (e) { if (signal?.aborted) return;