Skip to content

fix(clerk-js): prevent background token refresh from nuking sessions on mobile#8303

Draft
chriscanin wants to merge 3 commits intomainfrom
chris/fix-background-refresh-session-nuke
Draft

fix(clerk-js): prevent background token refresh from nuking sessions on mobile#8303
chriscanin wants to merge 3 commits intomainfrom
chris/fix-background-refresh-session-nuke

Conversation

@chriscanin
Copy link
Copy Markdown
Member

@chriscanin chriscanin commented Apr 13, 2026

Summary

Fixes an issue where iOS background thread throttling causes unexpected session destruction in Expo/React Native apps.

The bug: On iOS, background audio apps (e.g., sleep/dream trackers) have their JS event loop starved for hours. When the SDK's background token refresh timer eventually fires, it sends a request with stale credentials → 401 → handleUnauthenticated() cascades into destroying the session, even though it's still valid on the server (30-day lifetime). Users wake up to find they're signed out.

The fix: A single early return in #refreshTokenInBackground(), gated to headless/mobile runtimes only (Expo sets runtimeEnvironment: 'headless'). If the token has already expired when the refresh timer fires, the refresh cycle was starved by iOS background throttling — bail out instead of sending a request with stale credentials. The next foreground getToken() call handles token acquisition through the normal path with proper retry logic.

Why this is safe

  • Mobile-only: Gated to runtimeEnvironment === 'headless', which is only set by @clerk/expo for React Native. Web, Next.js, Remix, Chrome extension, etc. are completely unaffected.
  • Only fires when the refresh is already too late: During normal operation, the refresh timer fires at ~43s into a 60s token (before expiration). The early return only triggers when the token is ALREADY expired, meaning the timer fired late due to OS-level thread throttling.
  • No shared code paths changed: No changes to _baseFetch, Token.create, handleUnauthenticated, or any other shared infrastructure. The change is entirely within the private #refreshTokenInBackground() method in Session.ts.
  • Legitimate revocations still work: If a session is revoked, the next foreground getToken() call triggers handleUnauthenticated() through the normal path and signs the user out properly.
  • Background refresh is best-effort: It already catches and swallows all errors. Skipping a doomed refresh is strictly better than attempting one that nukes the session as a side effect.

Files changed

File Change
Session.ts Early return in #refreshTokenInBackground() when token is expired on headless runtime
Session.test.ts Unit test verifying background refresh is skipped when token is expired on headless runtime

How was this tested

  1. Unit test: Simulates iOS throttling by jumping the clock past token expiration, then firing the refresh timer. Verifies no API call is made on headless runtime. All 73 Session tests pass.
  2. Live iOS simulator test: Debug panel in Expo quickstart app intercepts /tokens with 401, waits for background refresh timer (~43s), confirms session survives.
  3. Maestro automated test: End-to-end flow on iOS simulator — sign in → simulate background refresh 401 → verify session survives → verify getToken() still works after.

Test plan

  • Unit test: background refresh skipped when token expired on headless runtime
  • All existing Session tests pass (73/73)
  • Live verification on iOS simulator
  • Maestro end-to-end test
  • Verify web apps are unaffected (runtimeEnvironment not set → early return never triggers)

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Apr 13, 2026 5:57pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 13, 2026

🦋 Changeset detected

Latest commit: aacaa32

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@clerk/clerk-js Patch
@clerk/chrome-extension Patch
@clerk/expo Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

…on mobile

On iOS, background thread throttling can starve the JS event loop for hours
(e.g., overnight audio apps). When the SDK's background refresh timer
eventually fires with stale credentials, the resulting 401 triggers
handleUnauthenticated() which destroys the session even though it's still
valid on the server.

Adds an early return in #refreshTokenInBackground(), gated to headless/mobile
runtimes only (Expo sets runtimeEnvironment to 'headless'). If the token has
already expired when the refresh timer fires, bail out instead of sending a
request with stale credentials. The next foreground getToken() call handles
token acquisition through the normal path with proper retry logic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant