Skip to content

fix(clerk-js): Prevent session cookie removal during offline token refresh#7912

Open
bratsos wants to merge 2 commits intomainfrom
alexbratsos/user-4744-investigate-random-sign-outs-in-the-dashboard-possibly
Open

fix(clerk-js): Prevent session cookie removal during offline token refresh#7912
bratsos wants to merge 2 commits intomainfrom
alexbratsos/user-4744-investigate-random-sign-outs-in-the-dashboard-possibly

Conversation

@bratsos
Copy link
Member

@bratsos bratsos commented Feb 23, 2026

Description

When offline, two code paths in Session.ts emit token:update events with empty tokens. AuthCookieService interprets empty tokens as signed-out and removes the __session cookie — even though the session is still valid server-side. On the next page load or visibility change, the missing cookie makes the user appear signed out.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • Bug Fixes

    • Prevented unexpected sign-outs when the browser temporarily loses network connectivity by improving offline token handling and retry behavior.
  • Tests

    • Added and updated tests covering offline retries, background refresh behavior, and recovery-from-offline scenarios to ensure tokens remain valid and events aren't emitted incorrectly.
  • Chores

    • Added a release notes entry for the patch.

@changeset-bot
Copy link

changeset-bot bot commented Feb 23, 2026

🦋 Changeset detected

Latest commit: fd118d9

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

@vercel
Copy link

vercel bot commented Feb 23, 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 Feb 27, 2026 7:09pm

Request Review

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 23, 2026

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7912

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7912

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7912

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7912

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7912

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7912

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@7912

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7912

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7912

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7912

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@7912

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7912

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7912

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7912

@clerk/react

npm i https://pkg.pr.new/@clerk/react@7912

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7912

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7912

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7912

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7912

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@7912

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7912

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7912

commit: fd118d9


describe('with offline browser and network failure', () => {
beforeEach(() => {
// Use real timers for offline tests to avoid unhandled rejection issues with retry logic
Copy link
Member

Choose a reason for hiding this comment

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

🎉

@bratsos bratsos force-pushed the alexbratsos/user-4744-investigate-random-sign-outs-in-the-dashboard-possibly branch from d134aeb to c230146 Compare February 25, 2026 12:05
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

Adds a changeset entry for a patch release. Modifies packages/clerk-js/src/core/resources/Session.ts to introduce ClerkRuntimeError, add offline-aware guards in _getToken, _fetchToken, _dispatchTokenEvents, and background refresh logic to avoid caching or dispatching empty tokens and to surface offline as explicit errors. Updates packages/clerk-js/src/core/resources/__tests__/Session.test.ts to use controlled timers, adjust offline retry tests, and add tests for background-refresh and recovery scenarios. No public API signatures were changed.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: preventing session cookie removal during offline token refresh, which is the core issue addressed in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@bratsos bratsos force-pushed the alexbratsos/user-4744-investigate-random-sign-outs-in-the-dashboard-possibly branch from 992a3ba to 2377201 Compare February 25, 2026 18:04
// Throw when offline and no token so retry() in getToken() can fire.
// Without this, _getToken returns null (success) and retry() never calls shouldRetry.
if (result === null && !isValidBrowserOnline()) {
throw new ClerkRuntimeError('Network request failed while offline', { code: 'network_error' });
Copy link
Member

Choose a reason for hiding this comment

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

Recommend that we re-use ClerkOfflineError here

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure about this. At this point throwing this error doesn't mean we're 100% offline, we just throw this so that retry's shouldRetry mechanism catches the error and retries 3 times (if the browser is offline during this). After the 3rd retry, if the browser is still offline, then we throw a ClerkOfflineError.

The way I see this is that when we throw this ClerkRuntimeError we're in a state where something went wrong with the network, but will give it some time, and we only throw ClerkOfflineError when there is no more attempts.

Copy link
Member

Choose a reason for hiding this comment

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

Got it, so this is not an error that the user would see?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes exactly - if it surfaces to the user it's gonna be a ClerkOfflineError. Unless I messed something up that is

return cachedToken.getRawString() || null;
result = cachedToken.getRawString() || null;
} else {
result = await this.#fetchToken(template, organizationId, tokenId, shouldDispatchTokenUpdate, skipCache);
Copy link
Member

Choose a reason for hiding this comment

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

does this throw if offline? should we avoid fetching if we're offline in the first place?

Copy link
Member Author

Choose a reason for hiding this comment

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

Great question! So that sent me through a rabbit hole and tl;dr: we can do it and if we throw here when we're offline, we'll trigger the retrying mechanism without firing network requests.

Additionally because of this exploration I figured a race condition that we had where the browser could go back online while mid-request, so I added a test for it and a fix here

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/clerk-js/src/core/resources/Session.ts`:
- Around line 451-453: The token-update path currently emits and caches/clears
when isValidBrowserOnline() is true even if cachedToken.getRawString() is empty;
change the guard so eventBus.emit(events.TokenUpdate, { token: cachedToken })
and any code that writes/clears __session only run when
cachedToken.getRawString() is truthy (i.e., require a non-empty token), and do
not rely on isValidBrowserOnline() as a substitute for a missing token. Apply
this same change to the other TokenUpdate/caching occurrences that use
cachedToken.getRawString() || isValidBrowserOnline() so all places (the
eventBus.emit call and the __session write/clear logic) first check
cachedToken.getRawString() before proceeding.

ℹ️ Review info

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2377201 and fd118d9.

📒 Files selected for processing (2)
  • packages/clerk-js/src/core/resources/Session.ts
  • packages/clerk-js/src/core/resources/__tests__/Session.test.ts

Comment on lines +451 to 453
if (shouldDispatchTokenUpdate && (cachedToken.getRawString() || isValidBrowserOnline())) {
eventBus.emit(events.TokenUpdate, { token: cachedToken });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Empty-token token:update can still fire after offline→online race.

The current guards allow dispatch/caching when getRawString() is empty but isValidBrowserOnline() is true. If the request failed while offline and connectivity flips before dispatch, this can still emit an empty token and clear __session.

Proposed fix
-      if (shouldDispatchTokenUpdate && (cachedToken.getRawString() || isValidBrowserOnline())) {
+      if (shouldDispatchTokenUpdate && cachedToken.getRawString()) {
         eventBus.emit(events.TokenUpdate, { token: cachedToken });
       }

@@
-    if (!token.getRawString() && !isValidBrowserOnline()) {
+    if (!token.getRawString()) {
       return;
     }

@@
-        if (!token.getRawString() && !isValidBrowserOnline()) {
+        if (!token.getRawString()) {
           return;
         }

Also applies to: 496-500, 565-579

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/clerk-js/src/core/resources/Session.ts` around lines 451 - 453, The
token-update path currently emits and caches/clears when isValidBrowserOnline()
is true even if cachedToken.getRawString() is empty; change the guard so
eventBus.emit(events.TokenUpdate, { token: cachedToken }) and any code that
writes/clears __session only run when cachedToken.getRawString() is truthy
(i.e., require a non-empty token), and do not rely on isValidBrowserOnline() as
a substitute for a missing token. Apply this same change to the other
TokenUpdate/caching occurrences that use cachedToken.getRawString() ||
isValidBrowserOnline() so all places (the eventBus.emit call and the __session
write/clear logic) first check cachedToken.getRawString() before proceeding.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants