fix: stop a storage-blocked browser blanking the app (#1137) - #1142
fix: stop a storage-blocked browser blanking the app (#1137)#1142MOHITKOURAV01 wants to merge 2 commits into
Conversation
TenantContext already had three guarded storage helpers, written for Aditya8369#843 with comments explaining why. Two other reads called localStorage.getItem bare. Reading localStorage is not merely "returns null when empty" -- the property access itself throws SecurityError when the browser blocks site data (Firefox's "Block cookies and site data", Safari private browsing, a partitioned iframe). The read at line 230 ran inside a useEffect with no try, so the throw escaped into React's commit phase and took down TenantProvider, which wraps the app. That is a blank page, not a degraded workspace picker. The Aditya8369#843 regression test has been failing on main. - Add readStoredTenantId(), a guarded raw read, and build readStoredTenant() on top of it. Left unvalidated on purpose: fetchTenants matches it against ids the API returned, which are not limited to KNOWN_TENANTS, so validation belongs at the call sites that want it. - Delete the mount effect's restore block. It read `tenants` out of the mount closure -- always the initial [], because resolving a promise does not rebind a captured variable -- so `tenants.length > 0` was never true and the body was dead code. fetchTenants already restores the saved workspace itself, against the data it just received. All the block contributed was the unguarded read. - Read `currentTenant` through a ref inside fetchTenants. Taking it as a dependency gave fetchTenants a new identity on every selection, and it is part of the context value, so each change invalidated the memo and re-rendered every consumer of useTenant(). The callback is now stable and its one caller can list it honestly. - Treat a non-array tenant response as an empty list. Tests: 13 new in TenantContext.storage.test.jsx. Six of them, plus the existing Aditya8369#843 test, fail against the code before this change.
|
@MOHITKOURAV01 is attempting to deploy a commit to the Aditya Mahajan's projects Team on Vercel. A member of the Team first needs to authorize it. |
Thank You for Your Contribution! 🎉Hi @MOHITKOURAV01, Thank you for opening this Pull Request and contributing to our project. We truly appreciate your efforts.
The maintainer @Aditya8369 will review your PR shortly! Happy Contributing! 🚀 |
…arded read Aditya8369#1054 is open against this same file and independently added a guarded raw reader for the API-driven paths. Same fix, different name. Using its name and its comment verbatim means git's three-way merge sees both branches making the identical change rather than two competing ones, so two of the four conflict hunks between these branches disappear. No behaviour change. The two that remain are the real differences this PR is for -- the ref-based fetchTenants and the deleted mount effect -- and they are noted on the PR with the resolution to take.
Overlap with #1054 — checked, and here is the resolutionBoth this PR and #1054 change #1054 independently added a guarded raw reader for the API-driven paths — the same fix as The two that remain are the real differences this PR is for. Take this branch's side on 1. // If we have API tenants and no current tenant is set, use the first one.
// Read through readRawStoredTenant() rather than touching localStorage
// directly: a browser with site data blocked throws on the bare call, and
// this one runs inside an async callback where nothing catches it (#843).
if (list.length > 0 && !currentTenantRef.current) {
const savedTenantId = readRawStoredTenant();2. The mount effect — take this branch's side outright. The block on #1054's side is the useEffect(() => {
fetchTenants();
}, [fetchTenants]);I ran the merged result: 54 tests pass — 25 from the existing If you'd rather avoid the conflict entirely, merging #1054 first and pinging me works too — |
Description
TenantContextalready knew thatlocalStoragecan throw — it has three guarded helpersbuilt for exactly that (
readStoredTenant,persistTenant,forgetTenant), each with atry/catchand a comment explaining why. Two other reads in the same file skipped themand called
localStorage.getItembare.Reading
localStorageis not merely "returns null when empty". The property access itselfthrows
SecurityError: The operation is insecure.when the browser is set to block sitedata — Firefox's Block cookies and site data, Safari private browsing with storage
disabled, or the widget embedded in a partitioned iframe. The read at line 230 ran inside a
useEffectwith notry, so the throw escaped into React's commit phase and unmountedTenantProvider, which wraps the app. Blank page.The repo's own #843 regression test has been failing on
mainbecause of it:Related Issue
Closes #1137
Type of Change
Changes Made
src/context/TenantContext.jsxreadStoredTenantId()— a guarded raw read, withreadStoredTenant()rebuilt on topof it. Every read in the module now goes through one place.
It deliberately does not validate against
KNOWN_TENANTS. That static list is for theoffline picker; the API returns workspaces this build has never heard of, and a stored one
of those still has to be restorable. Validation belongs at the call sites that want it —
readStoredTenant()applies it,fetchTenantsdoesn't. There's a test for the API-onlycase so this doesn't get "tidied up" later.
Deleted the mount effect's restore block. It could never have run:
Empty dependency array, so
tenantsinside the callback is the[]captured at mount.Resolving a promise doesn't rebind a captured variable — it schedules a state update for
the next render.
tenants.length > 0was never true and the body was dead code.It was also redundant:
fetchTenantsalready restores the saved workspace itself, fromthe
datait just received rather than from stale state. So the effect contributed oneunguarded storage read, one dead branch, and nothing else. The effect now just calls
fetchTenants().currentTenantread through a ref insidefetchTenants. Taking it as a dependencygave
fetchTenantsa new identity on every workspace selection — and it's part of thecontext value, so each change invalidated the
useMemoand re-rendered every consumer ofuseTenant(). The callback is stable now, which also means the mount effect can list itsdependency honestly instead of lying with
[].A non-array tenant response is treated as an empty list.
setTenants({...})followedby
tenants.find(...)elsewhere is aTypeErrorwaiting on a shape change.src/tests/contexts/TenantContext.storage.test.jsx(new, 13 tests)Storage-blocked mount, a
getItemthat only starts throwing after the initialiser (this isthe one that exercises the old line 230 specifically), workspace selection with no readable
storage, refused writes, restoring a stored workspace, falling back when the stored id is
gone, restoring an API-only workspace, not clobbering a manual selection, fetch-once-on-
mount,
fetchTenantsidentity stability, a rejected fetch, a non-array response, andsettings updates.
Testing
Against the code before this change, 6 of the new tests fail plus the existing #843 one:
The existing
TenantContext.test.jsxis unmodified — all 25 of its tests pass, includingthe one that was failing.
Note on CI
Lint, Build and Playwright are red on
mainand on every open PR (the 3 parse errors of#1129). Nothing here touches those files.