From fe3544fdb9fa01d68efa4dc22d40de90c55ffabe Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 4 Sep 2026 12:11:01 -0700 Subject: [PATCH] fix(js): complete offline sign-out Ensure local auth state reaches signed out when session removal fails with an offline network error. Preserve existing error handling for all other failures. --- .changeset/calm-otters-sign.md | 5 ++ .../clerk-js/src/core/__tests__/clerk.test.ts | 49 ++++++++++++++++++- packages/clerk-js/src/core/clerk.ts | 14 ++++-- 3 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 .changeset/calm-otters-sign.md diff --git a/.changeset/calm-otters-sign.md b/.changeset/calm-otters-sign.md new file mode 100644 index 00000000000..ce1462289a9 --- /dev/null +++ b/.changeset/calm-otters-sign.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Complete local sign-out while offline when cached resources surface a network error. diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 596c9cc0447..1daf950ac80 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -1,4 +1,4 @@ -import { ClerkOfflineError, EmailLinkErrorCodeStatus } from '@clerk/shared/error'; +import { ClerkOfflineError, ClerkRuntimeError, EmailLinkErrorCodeStatus } from '@clerk/shared/error'; import { ERROR_CODES } from '@clerk/shared/internal/clerk-js/constants'; import type { ActiveSessionResource, @@ -1290,6 +1290,53 @@ describe('Clerk singleton', () => { }, ); + it('completes local sign-out when removing sessions fails with an offline network error', async () => { + mockClientRemoveSessions.mockRejectedValue( + new ClerkRuntimeError('Network request failed.', { code: 'network_error' }), + ); + mockClientFetch.mockReturnValue( + Promise.resolve({ + ...clientTouchDefaults, + signedInSessions: [mockSession1], + sessions: [mockSession1], + destroy: mockClientDestroy, + removeSessions: mockClientRemoveSessions, + }), + ); + + const sut = new Clerk(productionPublishableKey); + sut.navigate = vi.fn(); + await sut.load(); + + await expect(sut.signOut()).resolves.toBeUndefined(); + + expect(sut.session).toBeNull(); + expect(sut.navigate).toHaveBeenCalledWith('/'); + }); + + it('rethrows non-network errors when removing sessions during sign-out', async () => { + const error = new ClerkRuntimeError('Request failed.', { code: 'unexpected_error' }); + mockClientRemoveSessions.mockRejectedValue(error); + mockClientFetch.mockReturnValue( + Promise.resolve({ + ...clientTouchDefaults, + signedInSessions: [mockSession1], + sessions: [mockSession1], + destroy: mockClientDestroy, + removeSessions: mockClientRemoveSessions, + }), + ); + + const sut = new Clerk(productionPublishableKey); + sut.navigate = vi.fn(); + await sut.load(); + + await expect(sut.signOut()).rejects.toBe(error); + + expect(sut.session).toBeUndefined(); + expect(sut.navigate).not.toHaveBeenCalled(); + }); + it('only removes the session that corresponds to the passed sessionId if it is not the current', async () => { mockClientFetch.mockReturnValue( Promise.resolve({ diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d957bf780e2..21745dfbd3d 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -789,10 +789,16 @@ export class Clerk implements ClerkInterface { if (!opts.sessionId || this.client.signedInSessions.length === 1) { this.#setTransitiveState(); - if (this.#options.experimental?.persistClient ?? true) { - await this.client.removeSessions(); - } else { - await this.client.destroy(); + try { + if (this.#options.experimental?.persistClient ?? true) { + await this.client.removeSessions(); + } else { + await this.client.destroy(); + } + } catch (err) { + if (!isClerkRuntimeError(err) || err.code !== 'network_error') { + throw err; + } } await executeSignOut();