Skip to content

fix: intermittent media 401 errors after token refresh or SW restart#548

Open
Just-Insane wants to merge 4 commits intoSableClient:devfrom
Just-Insane:fix/media-error-handling
Open

fix: intermittent media 401 errors after token refresh or SW restart#548
Just-Insane wants to merge 4 commits intoSableClient:devfrom
Just-Insane:fix/media-error-handling

Conversation

@Just-Insane
Copy link
Copy Markdown
Contributor

@Just-Insane Just-Insane commented Mar 26, 2026

Description

Fixes a chain of service worker session bugs that caused intermittent 401/404 errors on authenticated media:

  • Media requests no longer return 401 after a token refresh or SW restart
  • Session cache is now guaranteed to flush before the SW can be killed, fixing intermittent 401s on iOS
  • OIDC token refreshes are forwarded to the service worker as they happen
  • Fixed unhandled promise rejections from media decrypt failures

Fixes #

Type of change

  • 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:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings

AI disclosure:

  • Partially AI assisted (GitHub Copilot)

sw.ts: The setSession/clearSession cache writes are now wrapped in event.waitUntil so the browser keeps the SW alive until the caches.put resolves — preventing stale tokens surviving a mid-write SW kill on iOS. Added a requestSessionWithTimeout fallback in fetchMediaWithRetry for the window between a token refresh completing and the resulting setSession message arriving. index.tsx: Calls pushSessionToSW synchronously before React renders so the SW has a valid token before the first fetch event. ClientRoot.tsx: Wires pushSessionToSW into the SDK's onTokenRefresh callback 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.

@Just-Insane Just-Insane force-pushed the fix/media-error-handling branch from 8ecb7cc to 715e9e4 Compare March 26, 2026 03:36
@Just-Insane Just-Insane marked this pull request as ready for review March 26, 2026 03:40
@Just-Insane Just-Insane requested review from 7w1 and hazre as code owners March 26, 2026 03:40
@Just-Insane Just-Insane changed the title fix: suppress unhandled promise rejections and wire OIDC token refresh to service worker fix: OIDC/SSO Token + Media issues Mar 26, 2026
@Just-Insane Just-Insane marked this pull request as draft March 26, 2026 13:17
@Just-Insane Just-Insane force-pushed the fix/media-error-handling branch from ae8c97d to c177218 Compare March 26, 2026 13:46
@Just-Insane Just-Insane marked this pull request as ready for review March 26, 2026 14:08
Copilot AI review requested due to automatic review settings March 27, 2026 14:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 useAsyncCallback error handling behavior and update its tests.
  • Persist OIDC refresh_token/expires_in_ms into session state and wire a tokenRefreshFunction into 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.

@Just-Insane Just-Insane marked this pull request as draft March 27, 2026 15:03
Just-Insane added a commit to Just-Insane/Sable that referenced this pull request Mar 27, 2026
- 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
Just-Insane added a commit to Just-Insane/Sable that referenced this pull request Mar 27, 2026
- 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
@Just-Insane Just-Insane changed the title fix: OIDC/SSO Token + Media issues fix: complete SW session fix — OIDC token refresh, media 401 retry, and initial-load race Mar 28, 2026
@Just-Insane Just-Insane changed the title fix: complete SW session fix — OIDC token refresh, media 401 retry, and initial-load race fix: SW session persistence race, media 401 retry, OIDC token refresh wiring, and initial media load race Mar 28, 2026
@Just-Insane Just-Insane changed the title fix: SW session persistence race, media 401 retry, OIDC token refresh wiring, and initial media load race fix: intermittent media 401 errors after token refresh or SW restart Mar 28, 2026
@Just-Insane Just-Insane marked this pull request as ready for review March 28, 2026 02:52
Just-Insane added a commit to Just-Insane/Sable that referenced this pull request Mar 28, 2026
- 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
@Just-Insane Just-Insane force-pushed the fix/media-error-handling branch from 72110d9 to e41e126 Compare March 28, 2026 15:50
Just-Insane added a commit to Just-Insane/Sable that referenced this pull request Mar 28, 2026
- 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
@Just-Insane Just-Insane force-pushed the fix/media-error-handling branch from e41e126 to d7f4e8c Compare March 28, 2026 15:56
@Just-Insane Just-Insane marked this pull request as draft March 29, 2026 13:57
@Just-Insane Just-Insane marked this pull request as ready for review March 29, 2026 17:52
@Just-Insane Just-Insane force-pushed the fix/media-error-handling branch from 3106aaa to 3c413f0 Compare March 29, 2026 18:26
@Just-Insane Just-Insane force-pushed the fix/media-error-handling branch from 3c413f0 to abc22c5 Compare March 30, 2026 12:51
@7w1
Copy link
Copy Markdown
Member

7w1 commented Mar 31, 2026

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.

Copy link
Copy Markdown
Member

@7w1 7w1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update changesets or split into multiple prs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants