Skip to content

Commit 5b1cef2

Browse files
committed
Handle denied browser storage access
1 parent 4a7b4df commit 5b1cef2

4 files changed

Lines changed: 140 additions & 16 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@reflag/browser-sdk": patch
3+
---
4+
5+
Handle SecurityError when browser storage is unavailable or access is denied.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Test if the error is a security error returned by the browser when cookies or local storage are blocked.
3+
* Adapted from the Argos CI project.
4+
*/
5+
export function checkIsSecurityError(error: unknown): error is Error {
6+
return (
7+
error instanceof Error &&
8+
// Safari
9+
(error.name === "SecurityError" ||
10+
// Firefox
11+
error.name === "NS_ERROR_FAILURE" ||
12+
error.name === "NS_ERROR_ABORT")
13+
);
14+
}

‎packages/browser-sdk/src/storage.ts‎

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { IS_SERVER } from "./config";
2+
import { checkIsSecurityError } from "./security-error";
23

34
export type StorageAdapter = {
45
getItem(key: string): Promise<string | null>;
@@ -14,23 +15,75 @@ export function createNoopStorageAdapter(): StorageAdapter {
1415
};
1516
}
1617

17-
export function getLocalStorageAdapter(): StorageAdapter {
18-
if (
19-
typeof localStorage === "undefined" ||
20-
!("setItem" in localStorage) ||
21-
!("removeItem" in localStorage)
22-
) {
23-
throw new Error(
24-
"localStorage is not available. Provide a custom storage adapter.",
25-
);
18+
/**
19+
* Get an item from local storage safely.
20+
*/
21+
export function getItem(key: string): string | null {
22+
try {
23+
if (typeof localStorage !== "undefined" && localStorage) {
24+
return localStorage.getItem(key);
25+
}
26+
if (typeof sessionStorage !== "undefined" && sessionStorage) {
27+
return sessionStorage.getItem(key);
28+
}
29+
return null;
30+
} catch (error) {
31+
if (checkIsSecurityError(error)) {
32+
return null;
33+
}
34+
throw error;
35+
}
36+
}
37+
38+
/**
39+
* Set an item in local storage safely.
40+
*/
41+
export function setItem(key: string, value: string): boolean {
42+
try {
43+
if (typeof localStorage !== "undefined" && localStorage) {
44+
localStorage.setItem(key, value);
45+
return true;
46+
}
47+
if (typeof sessionStorage !== "undefined" && sessionStorage) {
48+
sessionStorage.setItem(key, value);
49+
return true;
50+
}
51+
return false;
52+
} catch (error) {
53+
if (checkIsSecurityError(error)) {
54+
return false;
55+
}
56+
throw error;
57+
}
58+
}
59+
60+
/**
61+
* Remove an item from local storage safely.
62+
*/
63+
export function removeItem(key: string): void {
64+
try {
65+
if (typeof localStorage !== "undefined") {
66+
localStorage.removeItem(key);
67+
}
68+
if (typeof sessionStorage !== "undefined") {
69+
sessionStorage.removeItem(key);
70+
}
71+
} catch (error) {
72+
if (checkIsSecurityError(error)) {
73+
return;
74+
}
75+
throw error;
2676
}
77+
}
78+
79+
export function getLocalStorageAdapter(): StorageAdapter {
2780
return {
28-
getItem: async (key) => localStorage.getItem(key),
81+
getItem: async (key) => getItem(key),
2982
setItem: async (key, value) => {
30-
localStorage.setItem(key, value);
83+
setItem(key, value);
3184
},
3285
removeItem: async (key) => {
33-
localStorage.removeItem(key);
86+
removeItem(key);
3487
},
3588
};
3689
}

‎packages/browser-sdk/test/storage.test.ts‎

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,41 @@ async function loadStorageModule() {
55
return import("../src/storage");
66
}
77

8+
const storageDescriptors = {
9+
localStorage: Object.getOwnPropertyDescriptor(globalThis, "localStorage"),
10+
sessionStorage: Object.getOwnPropertyDescriptor(globalThis, "sessionStorage"),
11+
};
12+
13+
function restoreStorageDescriptor(name: "localStorage" | "sessionStorage") {
14+
const descriptor = storageDescriptors[name];
15+
if (descriptor) {
16+
Object.defineProperty(globalThis, name, descriptor);
17+
} else {
18+
delete (globalThis as Record<string, unknown>)[name];
19+
}
20+
}
21+
22+
function defineThrowingStorage(
23+
name: "localStorage" | "sessionStorage",
24+
errorName = "SecurityError",
25+
) {
26+
Object.defineProperty(globalThis, name, {
27+
configurable: true,
28+
get() {
29+
const error = new Error("Access is denied for this document.");
30+
error.name = errorName;
31+
throw error;
32+
},
33+
});
34+
}
35+
836
describe("storage adapters", () => {
937
afterEach(() => {
1038
vi.unstubAllGlobals();
39+
restoreStorageDescriptor("localStorage");
40+
restoreStorageDescriptor("sessionStorage");
41+
localStorage.clear();
42+
sessionStorage.clear();
1143
});
1244

1345
it("noop adapter ignores writes", async () => {
@@ -22,14 +54,34 @@ describe("storage adapters", () => {
2254
expect(await adapter.getItem("key")).toBeNull();
2355
});
2456

25-
it("localStorage adapter throws when localStorage is unavailable", async () => {
57+
it("localStorage adapter falls back to sessionStorage when localStorage is unavailable", async () => {
2658
const { getLocalStorageAdapter } = await loadStorageModule();
2759
vi.stubGlobal("localStorage", undefined);
28-
expect(() => getLocalStorageAdapter()).toThrowError(
29-
"localStorage is not available. Provide a custom storage adapter.",
30-
);
60+
61+
const adapter = getLocalStorageAdapter();
62+
63+
await adapter.setItem("fallback", "ok");
64+
expect(sessionStorage.getItem("fallback")).toBe("ok");
65+
expect(await adapter.getItem("fallback")).toBe("ok");
66+
67+
await adapter.removeItem?.("fallback");
68+
expect(sessionStorage.getItem("fallback")).toBeNull();
3169
});
3270

71+
it.each(["SecurityError", "NS_ERROR_FAILURE", "NS_ERROR_ABORT"])(
72+
"localStorage adapter ignores %s when storage access is denied",
73+
async (errorName) => {
74+
const { getLocalStorageAdapter } = await loadStorageModule();
75+
defineThrowingStorage("localStorage", errorName);
76+
77+
const adapter = getLocalStorageAdapter();
78+
79+
expect(await adapter.getItem("key")).toBeNull();
80+
await expect(adapter.setItem("key", "value")).resolves.toBeUndefined();
81+
await expect(adapter.removeItem?.("key")).resolves.toBeUndefined();
82+
},
83+
);
84+
3385
it("default adapter falls back to noop on server runtimes", async () => {
3486
vi.resetModules();
3587
vi.stubGlobal("window", undefined);

0 commit comments

Comments
 (0)