fix: intermittent media 401 errors after token refresh or SW restart#548
fix: intermittent media 401 errors after token refresh or SW restart#548Just-Insane wants to merge 4 commits intoSableClient:devfrom
Conversation
8ecb7cc to
715e9e4
Compare
ae8c97d to
c177218
Compare
There was a problem hiding this comment.
Pull request overview
This PR targets media reliability by addressing token refresh propagation and service-worker handling of authenticated media fetches, plus reducing noisy “Uncaught (in promise)” console errors in media-loading UI flows.
Changes:
- Adjust
useAsyncCallbackerror handling behavior and update its tests. - Persist OIDC
refresh_token/expires_in_msinto session state and wire atokenRefreshFunctioninto the Matrix client, propagating refreshed access tokens to the service worker. - Add a SW media fetch wrapper that retries once on HTTP 401 using the latest in-memory/live session token.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/sw.ts |
Adds fetchMediaWithRetry() and routes media fetch paths through it to mitigate 401s during token-refresh propagation races. |
src/client/initMatrix.ts |
Passes refresh-token options into createClient and plumbs an optional onTokenRefresh callback to propagate new tokens outward. |
src/app/pages/client/ClientRoot.tsx |
Supplies the onTokenRefresh callback to update sessions state and push updated tokens to the SW. |
src/app/pages/auth/login/loginUtil.ts |
Stores refresh_token and expires_in_ms into the session record on login. |
src/app/hooks/useAsyncCallback.ts |
Changes error handling in the async wrapper (no longer rejects on error) and documents rationale. |
src/app/hooks/useAsyncCallback.test.tsx |
Updates error test to no longer expect a rejected promise. |
.changeset/fix-media-error-handling.md |
Adds a patch changeset describing the combined fixes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- useAsyncCallback: re-throw in catch to preserve rejection semantics; add no-op .catch wrapper in useAsyncCallback to suppress unhandled rejection warnings for fire-and-forget callers - useAsyncCallback.test: expect rejects.toBe(boom) to assert rejection - loginUtil: use != null guards for refresh_token / expires_in_ms to correctly handle zero/falsy-but-present values - initMatrix: use typeof === 'number' guard for expires_in_ms expiry calc - ClientRoot: pass activeSession.userId to both pushSessionToSW calls so the SW session record keeps a complete userId
- useAsyncCallback: re-throw in catch to preserve rejection semantics; add no-op .catch wrapper in useAsyncCallback to suppress unhandled rejection warnings for fire-and-forget callers - useAsyncCallback.test: expect rejects.toBe(boom) to assert rejection - loginUtil: use != null guards for refresh_token / expires_in_ms to correctly handle zero/falsy-but-present values - initMatrix: use typeof === 'number' guard for expires_in_ms expiry calc - ClientRoot: pass activeSession.userId to both pushSessionToSW calls so the SW session record keeps a complete userId
- useAsyncCallback: re-throw in catch to preserve rejection semantics; add no-op .catch wrapper in useAsyncCallback to suppress unhandled rejection warnings for fire-and-forget callers - useAsyncCallback.test: expect rejects.toBe(boom) to assert rejection - loginUtil: use != null guards for refresh_token / expires_in_ms to correctly handle zero/falsy-but-present values - initMatrix: use typeof === 'number' guard for expires_in_ms expiry calc - ClientRoot: pass activeSession.userId to both pushSessionToSW calls so the SW session record keeps a complete userId
72110d9 to
e41e126
Compare
- useAsyncCallback: re-throw in catch to preserve rejection semantics; add no-op .catch wrapper in useAsyncCallback to suppress unhandled rejection warnings for fire-and-forget callers - useAsyncCallback.test: expect rejects.toBe(boom) to assert rejection - loginUtil: use != null guards for refresh_token / expires_in_ms to correctly handle zero/falsy-but-present values - initMatrix: use typeof === 'number' guard for expires_in_ms expiry calc - ClientRoot: pass activeSession.userId to both pushSessionToSW calls so the SW session record keeps a complete userId
e41e126 to
d7f4e8c
Compare
3106aaa to
3c413f0
Compare
…te-sessions devtool
3c413f0 to
abc22c5
Compare
|
I think there might be some extraneous features in here? The whole new dev tools section, the UrlPreviewCard.tsx normalization changes and the sliding sync probe caching? I think this should either be split into multiple prs or the description should be updated and there should be more changesets. In general I think the code looks fine. |
7w1
left a comment
There was a problem hiding this comment.
update changesets or split into multiple prs
…e) (#611) > Related to closed PR #548 (`fix/media-error-handling`), which was split into smaller focused PRs. ### Description `injectionPoint: undefined` was set in `vite.config.ts` to silence a Workbox `AssertionError` about multiple `self.__WB_MANIFEST` occurrences (an `if` guard in `sw.ts` plus the `precacheAndRoute()` call = two matches; Workbox requires exactly one). The unintended consequence: Workbox never injected the precache manifest, so **no assets were precached** — every cold start hit the network instead of serving from cache, and offline use was broken entirely. Fix: remove the `if` guard in `sw.ts` (leaving exactly one `self.__WB_MANIFEST` reference) then remove `injectionPoint: undefined` from `vite.config.ts` to restore normal precaching behaviour. Fixes # #### Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] My changes generate no new warnings ### AI disclosure: - [x] Partially AI assisted (clarify which code was AI assisted and briefly explain what it does). - [ ] Fully AI generated (explain what all the generated code does in moderate detail). The root cause diagnosis (two `self.__WB_MANIFEST` references causing Workbox to silently skip injection) was identified with AI assistance. I verified the fix by checking Workbox's `injectionPoint` behaviour and confirming precaching was restored.
…er, and index.html (#609) > Related to closed PR #548 (`fix/media-error-handling`), which was split into smaller focused PRs. ### Description Without explicit `Cache-Control` headers, browsers apply heuristic caching to all responses — including `sw.js` and `index.html`. This causes stale service workers and stale app shells to persist after a deploy. Three header rules added to the Caddyfile: - `/assets/*` — `public, max-age=31536000, immutable`: content-hashed filenames change on every build, so these are safe to cache permanently. - `/sw.js` — `no-cache`: must always be revalidated so the browser picks up SW updates promptly. - `/index.html` — `no-cache`: must always be revalidated so the browser gets the latest asset hashes on load. Fixes # #### Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] My changes generate no new warnings ### AI disclosure: - [x] Partially AI assisted (clarify which code was AI assisted and briefly explain what it does). - [ ] Fully AI generated (explain what all the generated code does in moderate detail). The cache header rules were partially AI assisted. I verified the `immutable` directive is appropriate for content-hashed assets and that `no-cache` (revalidate-always, not no-store) is correct for `sw.js` and `index.html`.
Description
Fixes a chain of service worker session bugs that caused intermittent 401/404 errors on authenticated media:
Fixes #
Type of change
Checklist:
AI disclosure:
sw.ts: ThesetSession/clearSessioncache writes are now wrapped inevent.waitUntilso the browser keeps the SW alive until thecaches.putresolves — preventing stale tokens surviving a mid-write SW kill on iOS. Added arequestSessionWithTimeoutfallback infetchMediaWithRetryfor the window between a token refresh completing and the resultingsetSessionmessage arriving.index.tsx: CallspushSessionToSWsynchronously before React renders so the SW has a valid token before the first fetch event.ClientRoot.tsx: WirespushSessionToSWinto the SDK'sonTokenRefreshcallback so OIDC token refreshes propagate to the SW immediately. Decrypt failure and media error async paths now have proper rejection handling to avoid unhandled promise rejections.