Skip to content

Commit

Permalink
fix: handle inaccessible localStorage gracefully (#10)
Browse files Browse the repository at this point in the history
* fix: cover the case were accessing `localStorage` can throw error in Node

Internally in `jsdom` has a getter function defined for `localStorage`
that can in some circumstances throw a runtime error that is not handled
by the package

The code has changed to use a function to qeury whether `localStorage` is
available in the runtime context. If accessing `localStorage` global throws
an error it returns an error otherwise `true`

fixes #9

* fix(localStorage): merge tests when localStorage is undefined

* chore(CookieStore): change function name to "supportsLocalStorage"

Co-authored-by: Weyert de Boer <[email protected]>
Co-authored-by: Artem Zakharchenko <[email protected]>
  • Loading branch information
3 people authored Jan 19, 2022
1 parent ea8a387 commit 80abf96
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 70 deletions.
73 changes: 43 additions & 30 deletions src/CookieStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,26 @@ interface HeadersLike {
interface ResponseLike {
headers: HeadersLike
}
type Store = Map<string, StoreEntry>
type StoreEntry = Map<string, Cookie>
type CookieString = Omit<Cookie, 'expires'> & { expires?: string }

export type Store = Map<string, StoreEntry>
export type StoreEntry = Map<string, Cookie>
export type CookieString = Omit<Cookie, 'expires'> & { expires?: string }

export const PERSISTENCY_KEY = 'MSW_COOKIE_STORE'

const SUPPORTS_LOCAL_STORAGE = typeof localStorage !== 'undefined'
function supportsLocalStorage() {
try {
if (localStorage == null) {
return false
}

localStorage.setItem('test', 'test')
localStorage.getItem('test')
return true
} catch (error) {
return false
}
}

class CookieStore {
private store: Store
Expand Down Expand Up @@ -95,7 +108,6 @@ class CookieStore {
*/
getAll(): Store {
this.deleteExpiredCookies()

return this.store
}

Expand All @@ -118,33 +130,35 @@ class CookieStore {
* Hydrates the virtual cookie store from the `localStorage` if defined.
*/
hydrate(): void {
if (!SUPPORTS_LOCAL_STORAGE) {
if (!supportsLocalStorage()) {
return
}

const persistedCookies = localStorage.getItem(PERSISTENCY_KEY)

if (persistedCookies) {
try {
const parsedCookies: [string, [string, CookieString][]][] = JSON.parse(
persistedCookies,
)
if (!persistedCookies) {
return
}

parsedCookies.forEach(([origin, cookies]) => {
this.store.set(
origin,
new Map(
cookies.map(([token, { expires, ...cookie }]) => [
token,
expires === undefined
? cookie
: { ...cookie, expires: new Date(expires) },
]),
),
)
})
} catch (error) {
console.warn(`
try {
const parsedCookies: [string, [string, CookieString][]][] =
JSON.parse(persistedCookies)

parsedCookies.forEach(([origin, cookies]) => {
this.store.set(
origin,
new Map(
cookies.map(([token, { expires, ...cookie }]) => [
token,
expires === undefined
? cookie
: { ...cookie, expires: new Date(expires) },
]),
),
)
})
} catch (error) {
console.warn(`
[virtual-cookie] Failed to parse a stored cookie from the localStorage (key "${PERSISTENCY_KEY}").
Stored value:
Expand All @@ -154,8 +168,7 @@ Thrown exception:
${error}
Invalid value has been removed from localStorage to prevent subsequent failed parsing attempts.`)
localStorage.removeItem(PERSISTENCY_KEY)
}
localStorage.removeItem(PERSISTENCY_KEY)
}
}

Expand All @@ -164,7 +177,7 @@ Invalid value has been removed from localStorage to prevent subsequent failed pa
* so they are available on the next page load.
*/
persist(): void {
if (!SUPPORTS_LOCAL_STORAGE) {
if (!supportsLocalStorage()) {
return
}

Expand Down Expand Up @@ -194,4 +207,4 @@ Invalid value has been removed from localStorage to prevent subsequent failed pa
}
}

export default new CookieStore()
export const store = new CookieStore()
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { default as store, PERSISTENCY_KEY } from './CookieStore'
export * from './CookieStore'
45 changes: 45 additions & 0 deletions test/localStorage-undefined.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Store, store } from '../src'

const originalLocalStorage: Storage = localStorage

beforeAll(() => {
Object.defineProperty(window, 'localStorage', {
value: undefined,
writable: true,
})
})

afterAll(() => {
Object.defineProperty(window, 'localStorage', {
value: originalLocalStorage,
})
})

it('adds response cookies to the store even if "localStorage" is unavailable', () => {
store.add(
new Request('https://example.com'),
new Response(null, {
headers: new Headers({
'Set-Cookie': 'cookieName=abc-123',
}),
}),
)
store.persist()

expect(store.getAll()).toEqual<Store>(
new Map([
[
'https://example.com',
new Map([
[
'cookieName',
{
name: 'cookieName',
value: 'abc-123',
},
],
]),
],
]),
)
})
39 changes: 0 additions & 39 deletions test/undefined-local-storage.test.ts

This file was deleted.

0 comments on commit 80abf96

Please sign in to comment.