From 8fd844e5cdc8ca694ccc353bbb24173c158dae55 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Fri, 13 Mar 2026 20:39:03 -0400 Subject: [PATCH 1/3] fix: unhandled promise rejection in media blob cache Fixes unhandled promise rejection when multiple components fetch the same media URL and the initial request fails with 401. ### Root Cause When a blob fetch request was in-flight and failed, subsequent callers awaiting the same promise would receive an unhandled rejection because the await was outside the try-catch block. ### Changes 1. useBlobCache: Wrap inflightRequests.get() await in try-catch 2. useBlobCache: Improve error message to include HTTP status 3. Service Worker: Add logging for session retrieval failures to diagnose authentication issues with media requests Addresses Sentry issue with 401 errors on authenticated media endpoints. --- src/app/hooks/useBlobCache.ts | 12 +++++++++--- src/sw.ts | 17 +++++++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/app/hooks/useBlobCache.ts b/src/app/hooks/useBlobCache.ts index a3d5493cf..7b40e292c 100644 --- a/src/app/hooks/useBlobCache.ts +++ b/src/app/hooks/useBlobCache.ts @@ -23,15 +23,21 @@ export function useBlobCache(url?: string): string | undefined { const fetchBlob = async () => { if (inflightRequests.has(url)) { - const existingBlobUrl = await inflightRequests.get(url); - if (isMounted) setCacheState({ sourceUrl: url, blobUrl: existingBlobUrl }); + try { + const existingBlobUrl = await inflightRequests.get(url); + if (isMounted) setCacheState({ sourceUrl: url, blobUrl: existingBlobUrl }); + } catch { + // Inflight request failed, silently ignore (consistent with fetchBlob behavior) + } return; } const requestPromise = (async () => { try { const res = await fetch(url, { mode: 'cors' }); - if (!res.ok) throw new Error(); + if (!res.ok) { + throw new Error(`Failed to fetch blob: ${res.status} ${res.statusText}`); + } const blob = await res.blob(); const objectUrl = URL.createObjectURL(blob); diff --git a/src/sw.ts b/src/sw.ts index 9f3314889..4486f418c 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -91,9 +91,11 @@ async function cleanupDeadClients() { function setSession(clientId: string, accessToken: unknown, baseUrl: unknown) { if (typeof accessToken === 'string' && typeof baseUrl === 'string') { sessions.set(clientId, { accessToken, baseUrl }); + console.debug('[SW] setSession: stored', clientId, baseUrl); } else { // Logout or invalid session sessions.delete(clientId); + console.debug('[SW] setSession: removed', clientId); } const resolveSession = clientToResolve.get(clientId); @@ -124,12 +126,18 @@ async function requestSessionWithTimeout( timeoutMs = 3000 ): Promise { const client = await self.clients.get(clientId); - if (!client) return undefined; + if (!client) { + console.warn('[SW] requestSessionWithTimeout: client not found', clientId); + return undefined; + } const sessionPromise = requestSession(client); const timeout = new Promise((resolve) => { - setTimeout(() => resolve(undefined), timeoutMs); + setTimeout(() => { + console.warn('[SW] requestSessionWithTimeout: timed out after', timeoutMs, 'ms', clientId); + resolve(undefined); + }, timeoutMs); }); return Promise.race([sessionPromise, timeout]); @@ -274,6 +282,11 @@ self.addEventListener('fetch', (event: FetchEvent) => { if (s && validMediaRequest(url, s.baseUrl)) { return fetch(url, { ...fetchConfig(s.accessToken), redirect }); } + console.warn( + '[SW fetch] No valid session for media request', + { url, clientId, hasSession: !!s }, + 'falling back to unauthenticated fetch' + ); return fetch(event.request); }) ); From 24fbbc0dc2dd08c64fac542401b51c30c284f348 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Fri, 13 Mar 2026 20:54:33 -0400 Subject: [PATCH 2/3] Add automatic retry for chunk loading failures - Global error handler catches chunk load errors - Auto-reload on first 2 failures without user intervention - On 3rd failure, let error bubble to Sentry error boundary - Uses sessionStorage to track retry count across reloads - Clears counter on successful page load - Prevents stale HTML from breaking app after deployments --- src/index.tsx | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/index.tsx b/src/index.tsx index 76ebaf91b..53944ef3f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -139,6 +139,41 @@ const injectIOSMetaTags = () => { injectIOSMetaTags(); +// Handle chunk loading failures with automatic retry +const CHUNK_RETRY_KEY = 'cinny_chunk_retry_count'; +const MAX_CHUNK_RETRIES = 2; + +window.addEventListener('error', (event) => { + // Check if this is a chunk loading error + const isChunkLoadError = + event.message?.includes('dynamically imported module') || + event.message?.includes('Failed to fetch') || + (event.error?.name === 'ChunkLoadError'); + + if (isChunkLoadError) { + const retryCount = parseInt(sessionStorage.getItem(CHUNK_RETRY_KEY) ?? '0', 10); + + if (retryCount < MAX_CHUNK_RETRIES) { + // Increment retry count and reload + sessionStorage.setItem(CHUNK_RETRY_KEY, String(retryCount + 1)); + log.warn(`Chunk load failed, reloading (attempt ${retryCount + 1}/${MAX_CHUNK_RETRIES})`); + window.location.reload(); + + // Prevent default error handling since we're reloading + event.preventDefault(); + } else { + // Max retries exceeded, clear counter and let error bubble up + sessionStorage.removeItem(CHUNK_RETRY_KEY); + log.error('Chunk load failed after max retries, showing error'); + } + } +}); + +// Clear chunk retry counter on successful page load +window.addEventListener('load', () => { + sessionStorage.removeItem(CHUNK_RETRY_KEY); +}); + const mountApp = () => { const rootContainer = document.getElementById('root'); From f67abc05d76d4e0025b2898edcc678dea121a1dc Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Fri, 13 Mar 2026 22:25:30 -0400 Subject: [PATCH 3/3] fix: format src/index.tsx and add changeset --- .changeset/fix-media-chunk-errors.md | 5 +++++ src/index.tsx | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-media-chunk-errors.md diff --git a/.changeset/fix-media-chunk-errors.md b/.changeset/fix-media-chunk-errors.md new file mode 100644 index 000000000..d6c55186a --- /dev/null +++ b/.changeset/fix-media-chunk-errors.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fixed unhandled promise rejections in media blob cache and added automatic retry for chunk loading failures after deployments. diff --git a/src/index.tsx b/src/index.tsx index 53944ef3f..3248458ba 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -148,17 +148,17 @@ window.addEventListener('error', (event) => { const isChunkLoadError = event.message?.includes('dynamically imported module') || event.message?.includes('Failed to fetch') || - (event.error?.name === 'ChunkLoadError'); + event.error?.name === 'ChunkLoadError'; if (isChunkLoadError) { const retryCount = parseInt(sessionStorage.getItem(CHUNK_RETRY_KEY) ?? '0', 10); - + if (retryCount < MAX_CHUNK_RETRIES) { // Increment retry count and reload sessionStorage.setItem(CHUNK_RETRY_KEY, String(retryCount + 1)); log.warn(`Chunk load failed, reloading (attempt ${retryCount + 1}/${MAX_CHUNK_RETRIES})`); window.location.reload(); - + // Prevent default error handling since we're reloading event.preventDefault(); } else {