diff --git a/.changeset/fix-sw-media-auth-401.md b/.changeset/fix-sw-media-auth-401.md new file mode 100644 index 00000000..5390991b --- /dev/null +++ b/.changeset/fix-sw-media-auth-401.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fix service worker authenticated media requests returning 401 errors after SW restart or when session data is missing/stale. diff --git a/src/sw.ts b/src/sw.ts index 064b293f..a7a75051 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -118,6 +118,14 @@ type SessionInfo = { */ const sessions = new Map(); +/** + * Session pre-loaded from cache on SW activation. Acts as an immediate + * fallback so media fetches don't 401 during the window between SW restart + * and the first live setSession message from the page. + * Cleared as soon as any real setSession call comes in. + */ +let preloadedSession: SessionInfo | undefined; + const clientToResolve = new Map void>(); const clientToSessionPromise = new Map>(); @@ -142,12 +150,15 @@ function setSession(clientId: string, accessToken: unknown, baseUrl: unknown, us userId: typeof userId === 'string' ? userId : undefined, }; sessions.set(clientId, info); + // A real session has arrived — discard the preloaded fallback. + preloadedSession = undefined; console.debug('[SW] setSession: stored', clientId, baseUrl); // Persist so push-event fetches work after iOS restarts the SW. persistSession(info).catch(() => undefined); } else { // Logout or invalid session sessions.delete(clientId); + preloadedSession = undefined; console.debug('[SW] setSession: removed', clientId); clearPersistedSession().catch(() => undefined); } @@ -460,6 +471,15 @@ self.addEventListener('activate', (event: ExtendableEvent) => { (async () => { await self.clients.claim(); await cleanupDeadClients(); + // Pre-load the persisted session into memory so that media fetches arriving + // before the first setSession message from the page are immediately + // authenticated rather than falling through to a 3-second timeout. + preloadedSession = await loadPersistedSession(); + // Proactively request sessions from all window clients so the sessions Map + // is pre-populated after a SW restart, rather than waiting for the first + // media fetch to trigger requestSessionWithTimeout. + const windowClients = await self.clients.matchAll({ type: 'window' }); + windowClients.forEach((client) => client.postMessage({ type: 'requestSession' })); })() ); }); @@ -579,7 +599,6 @@ self.addEventListener('fetch', (event: FetchEvent) => { if (method !== 'GET' || !mediaPath(url)) return; const { clientId } = event; - if (!clientId) return; // For browser sub-resource loads (images, video, audio, etc.), 'follow' is // the correct mode: the auth header is sent to the Matrix server which owns @@ -588,11 +607,9 @@ self.addEventListener('fetch', (event: FetchEvent) => { // the browser cannot render as an /