From 3eb6e94725d2cbf0f6492dc72178d0c00554b018 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Wed, 18 Mar 2026 15:48:35 +0200 Subject: [PATCH] feat(clerk-js,shared): Remove expired_token retry flow The previous session token is now always sent in the /tokens POST body (via the `token` param), so the backend no longer needs to request it via a 422 missing_expired_token error and retry. Removes: - MissingExpiredTokenError class and its re-export from @clerk/shared - The catch-and-retry logic in Session.#createTokenResolver - 4 related tests in Session.test.ts --- .changeset/remove-expired-token-retry.md | 6 + .../clerk-js/src/core/resources/Session.ts | 19 +-- .../core/resources/__tests__/Session.test.ts | 149 +----------------- packages/shared/src/error.ts | 1 - .../src/errors/missingExpiredTokenError.ts | 45 ------ 5 files changed, 9 insertions(+), 211 deletions(-) create mode 100644 .changeset/remove-expired-token-retry.md delete mode 100644 packages/shared/src/errors/missingExpiredTokenError.ts diff --git a/.changeset/remove-expired-token-retry.md b/.changeset/remove-expired-token-retry.md new file mode 100644 index 00000000000..2cca51d6f12 --- /dev/null +++ b/.changeset/remove-expired-token-retry.md @@ -0,0 +1,6 @@ +--- +'@clerk/shared': patch +'@clerk/clerk-js': patch +--- + +Remove `expired_token` retry flow and `MissingExpiredTokenError`. The previous session token is now always sent in the `/tokens` POST body, so the retry-with-expired-token fallback is no longer needed. diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index ea5e796dbb8..50464781a2c 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -1,13 +1,6 @@ import { createCheckAuthorization } from '@clerk/shared/authorization'; import { isBrowserOnline, isValidBrowserOnline } from '@clerk/shared/browser'; -import { - ClerkOfflineError, - ClerkRuntimeError, - ClerkWebAuthnError, - is4xxError, - is429Error, - MissingExpiredTokenError, -} from '@clerk/shared/error'; +import { ClerkOfflineError, ClerkRuntimeError, ClerkWebAuthnError, is4xxError, is429Error } from '@clerk/shared/error'; import { convertJSONToPublicKeyRequestOptions, serializePublicKeyCredentialAssertion, @@ -481,16 +474,8 @@ export class Session extends BaseResource implements SessionResource { const path = template ? `${this.path()}/tokens/${template}` : `${this.path()}/tokens`; // TODO: update template endpoint to accept organizationId const params: Record = template ? {} : { organizationId: organizationId ?? null }; - const lastActiveToken = this.lastActiveToken?.getRawString(); - - const tokenResolver = Token.create(path, params, skipCache ? { debug: 'skip_cache' } : undefined).catch(e => { - if (MissingExpiredTokenError.is(e) && lastActiveToken) { - return Token.create(path, { ...params }, { expired_token: lastActiveToken }); - } - throw e; - }); - return tokenResolver; + return Token.create(path, params, skipCache ? { debug: 'skip_cache' } : undefined); } #dispatchTokenEvents(token: TokenResource, shouldDispatch: boolean): void { diff --git a/packages/clerk-js/src/core/resources/__tests__/Session.test.ts b/packages/clerk-js/src/core/resources/__tests__/Session.test.ts index 4ccae5510e2..01a08e76c46 100644 --- a/packages/clerk-js/src/core/resources/__tests__/Session.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/Session.test.ts @@ -1,4 +1,4 @@ -import { ClerkAPIResponseError, ClerkOfflineError } from '@clerk/shared/error'; +import { ClerkOfflineError } from '@clerk/shared/error'; import type { InstanceType, OrganizationJSON, SessionJSON } from '@clerk/shared/types'; import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; @@ -1522,153 +1522,6 @@ describe('Session', () => { }); }); - describe('origin outage mode fallback', () => { - let dispatchSpy: ReturnType; - let fetchSpy: ReturnType; - - beforeEach(() => { - SessionTokenCache.clear(); - dispatchSpy = vi.spyOn(eventBus, 'emit'); - fetchSpy = vi.spyOn(BaseResource, '_fetch' as any); - BaseResource.clerk = clerkMock() as any; - }); - - afterEach(() => { - dispatchSpy?.mockRestore(); - fetchSpy?.mockRestore(); - BaseResource.clerk = null as any; - }); - - it('should retry with expired token when API returns 422 with missing_expired_token error', async () => { - const session = new Session({ - status: 'active', - id: 'session_1', - object: 'session', - user: createUser({}), - last_active_organization_id: null, - last_active_token: { object: 'token', jwt: mockJwt }, - actor: null, - created_at: new Date().getTime(), - updated_at: new Date().getTime(), - } as SessionJSON); - - SessionTokenCache.clear(); - - const errorResponse = new ClerkAPIResponseError('Missing expired token', { - data: [ - { code: 'missing_expired_token', message: 'Missing expired token', long_message: 'Missing expired token' }, - ], - status: 422, - }); - fetchSpy.mockRejectedValueOnce(errorResponse); - - fetchSpy.mockResolvedValueOnce({ object: 'token', jwt: mockJwt }); - - await session.getToken(); - - expect(fetchSpy).toHaveBeenCalledTimes(2); - - expect(fetchSpy.mock.calls[0][0]).toMatchObject({ - path: '/client/sessions/session_1/tokens', - method: 'POST', - body: { organizationId: null }, - }); - - expect(fetchSpy.mock.calls[1][0]).toMatchObject({ - path: '/client/sessions/session_1/tokens', - method: 'POST', - body: { organizationId: null }, - search: { expired_token: mockJwt }, - }); - }); - - it('should not retry with expired token when lastActiveToken is not available', async () => { - const session = new Session({ - status: 'active', - id: 'session_1', - object: 'session', - user: createUser({}), - last_active_organization_id: null, - last_active_token: null, - actor: null, - created_at: new Date().getTime(), - updated_at: new Date().getTime(), - } as unknown as SessionJSON); - - SessionTokenCache.clear(); - - const errorResponse = new ClerkAPIResponseError('Missing expired token', { - data: [ - { code: 'missing_expired_token', message: 'Missing expired token', long_message: 'Missing expired token' }, - ], - status: 422, - }); - fetchSpy.mockRejectedValue(errorResponse); - - await expect(session.getToken()).rejects.toMatchObject({ - status: 422, - errors: [{ code: 'missing_expired_token' }], - }); - - expect(fetchSpy).toHaveBeenCalledTimes(1); - }); - - it('should not retry with expired token for non-422 errors', async () => { - const session = new Session({ - status: 'active', - id: 'session_1', - object: 'session', - user: createUser({}), - last_active_organization_id: null, - last_active_token: { object: 'token', jwt: mockJwt }, - actor: null, - created_at: new Date().getTime(), - updated_at: new Date().getTime(), - } as SessionJSON); - - SessionTokenCache.clear(); - - const errorResponse = new ClerkAPIResponseError('Bad request', { - data: [{ code: 'bad_request', message: 'Bad request', long_message: 'Bad request' }], - status: 400, - }); - fetchSpy.mockRejectedValueOnce(errorResponse); - - await expect(session.getToken()).rejects.toThrow(ClerkAPIResponseError); - - expect(fetchSpy).toHaveBeenCalledTimes(1); - }); - - it('should not retry with expired token when error code is different', async () => { - const session = new Session({ - status: 'active', - id: 'session_1', - object: 'session', - user: createUser({}), - last_active_organization_id: null, - last_active_token: { object: 'token', jwt: mockJwt }, - actor: null, - created_at: new Date().getTime(), - updated_at: new Date().getTime(), - } as unknown as SessionJSON); - - SessionTokenCache.clear(); - - const errorResponse = new ClerkAPIResponseError('Validation failed', { - data: [{ code: 'validation_error', message: 'Validation failed', long_message: 'Validation failed' }], - status: 422, - }); - fetchSpy.mockRejectedValue(errorResponse); - - await expect(session.getToken()).rejects.toMatchObject({ - status: 422, - errors: [{ code: 'validation_error' }], - }); - - expect(fetchSpy).toHaveBeenCalledTimes(1); - }); - }); - describe('agent', () => { it('sets agent to null when actor is null', () => { const session = new Session({ diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index 37eb5e41bb7..bdf633991ad 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -3,7 +3,6 @@ export { errorToJSON, parseError, parseErrors } from './errors/parseError'; export { ClerkAPIError, isClerkAPIError } from './errors/clerkApiError'; export { ClerkAPIResponseError, isClerkAPIResponseError } from './errors/clerkApiResponseError'; export { ClerkError, isClerkError } from './errors/clerkError'; -export { MissingExpiredTokenError } from './errors/missingExpiredTokenError'; export { ClerkOfflineError } from './errors/clerkOfflineError'; export { buildErrorThrower, type ErrorThrower, type ErrorThrowerOptions } from './errors/errorThrower'; diff --git a/packages/shared/src/errors/missingExpiredTokenError.ts b/packages/shared/src/errors/missingExpiredTokenError.ts deleted file mode 100644 index 2b3d15396e0..00000000000 --- a/packages/shared/src/errors/missingExpiredTokenError.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ClerkAPIResponseError, isClerkAPIResponseError } from './clerkApiResponseError'; - -/** - * Error class representing a missing expired token error from the API. - * This error occurs when the server requires an expired token to mint a new session token. - * - * Use the static `is` method to check if a ClerkAPIResponseError matches this error type. - * - * @example - * ```typescript - * if (MissingExpiredTokenError.is(error)) { - * // Handle the missing expired token error - * } - * ``` - */ -export class MissingExpiredTokenError extends ClerkAPIResponseError { - static kind = 'MissingExpiredTokenError'; - static readonly ERROR_CODE = 'missing_expired_token' as const; - static readonly STATUS = 422 as const; - - /** - * Type guard to check if an error is a MissingExpiredTokenError. - * This checks the error's properties (status and error code) rather than instanceof, - * allowing it to work with ClerkAPIResponseError instances thrown from the API layer. - * - * @example - * ```typescript - * try { - * await someApiCall(); - * } catch (e) { - * if (MissingExpiredTokenError.is(e)) { - * // e is typed as ClerkAPIResponseError with the specific error properties - * } - * } - * ``` - */ - static is(err: unknown): err is ClerkAPIResponseError { - return ( - isClerkAPIResponseError(err) && - err.status === MissingExpiredTokenError.STATUS && - err.errors.length > 0 && - err.errors[0].code === MissingExpiredTokenError.ERROR_CODE - ); - } -}