Skip to content

Commit

Permalink
Merge pull request #2974 from digitalfabrik/2924-security-error
Browse files Browse the repository at this point in the history
2924: Catch SecurityError when accessing localStorage
  • Loading branch information
steffenkleinle authored Nov 11, 2024
2 parents ad6c6fe + 113fa06 commit fd18304
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 5 deletions.
5 changes: 5 additions & 0 deletions release-notes/unreleased/2924-security-error.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
issue_key: 2924
show_in_stores: false
platforms:
- web
en: Fix crashes if local storage is not available
75 changes: 75 additions & 0 deletions web/src/hooks/__tests__/useLocalStorage.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { fireEvent, render } from '@testing-library/react'
import React from 'react'

import Button from '../../components/base/Button'
import useLocalStorage from '../useLocalStorage'

describe('useLocalStorage', () => {
const key = 'my_storage_key'
const MockComponent = () => {
const { value, updateLocalStorageItem } = useLocalStorage({ key, initialValue: 0 })
return (
<div>
{value}
<Button label='increment' onClick={() => updateLocalStorageItem(value + 1)}>
Increment
</Button>
</div>
)
}

beforeEach(() => {
jest.clearAllMocks()
localStorage.clear()
})

it('should correctly set initial value and update value', () => {
const { getByText } = render(<MockComponent />)

expect(getByText(0)).toBeTruthy()
expect(localStorage.getItem(key)).toBe('0')

fireEvent.click(getByText('Increment'))

expect(getByText(1)).toBeTruthy()
expect(localStorage.getItem(key)).toBe('1')

fireEvent.click(getByText('Increment'))
fireEvent.click(getByText('Increment'))
fireEvent.click(getByText('Increment'))

expect(getByText(4)).toBeTruthy()
expect(localStorage.getItem(key)).toBe('4')
})

it('should not use initial value if already set', () => {
localStorage.setItem(key, '10')
const { getByText } = render(<MockComponent />)

expect(getByText(10)).toBeTruthy()
expect(localStorage.getItem(key)).toBe('10')

fireEvent.click(getByText('Increment'))

expect(getByText(11)).toBeTruthy()
expect(localStorage.getItem(key)).toBe('11')
})

it('should continue to work even if local storage is not usable', () => {
localStorage.getItem = () => {
throw new Error('SecurityError')
}
localStorage.setItem = () => {
throw new Error('SecurityError')
}
const { getByText } = render(<MockComponent />)

expect(getByText(0)).toBeTruthy()
expect(localStorage.getItem(key)).toBe('0')

fireEvent.click(getByText('Increment'))

expect(getByText(1)).toBeTruthy()
expect(localStorage.getItem(key)).toBe('1')
})
})
30 changes: 25 additions & 5 deletions web/src/hooks/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useState, useCallback } from 'react'

import { reportError } from '../utils/sentry'

type UseLocalStorageProps<T> = {
key: string
initialValue: T
Expand All @@ -12,17 +14,35 @@ type UseLocalStorageReturn<T> = {

const useLocalStorage = <T>({ key, initialValue }: UseLocalStorageProps<T>): UseLocalStorageReturn<T> => {
const [value, setValue] = useState<T>(() => {
const localStorageItem = localStorage.getItem(key)
if (localStorageItem) {
return JSON.parse(localStorageItem)
try {
const localStorageItem = localStorage.getItem(key)
if (localStorageItem) {
return JSON.parse(localStorageItem)
}
localStorage.setItem(key, JSON.stringify(initialValue))
} catch (e) {
// Prevent the following error crashing the app if the browser blocks access to local storage (see #2924)
// SecurityError: Failed to read the 'localStorage' property from 'Window': Access is denied for this document.
const accessDenied = e instanceof Error && e.message.includes('Access is denied for this document')
if (!accessDenied) {
reportError(e)
}
}
localStorage.setItem(key, JSON.stringify(initialValue))
return initialValue
})

const updateLocalStorageItem = useCallback(
(newValue: T) => {
localStorage.setItem(key, JSON.stringify(newValue))
try {
localStorage.setItem(key, JSON.stringify(newValue))
} catch (e) {
// Prevent the following error crashing the app if the browser blocks access to local storage (see #2924)
// SecurityError: Failed to read the 'localStorage' property from 'Window': Access is denied for this document.
const accessDenied = e instanceof Error && e.message.includes('Access is denied for this document')
if (!accessDenied) {
reportError(e)
}
}
setValue(newValue)
},
[key],
Expand Down

0 comments on commit fd18304

Please sign in to comment.