Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/remove-expired-token-retry.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 2 additions & 17 deletions packages/clerk-js/src/core/resources/Session.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<string, string | null> = 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 {
Expand Down
149 changes: 1 addition & 148 deletions packages/clerk-js/src/core/resources/__tests__/Session.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -1522,153 +1522,6 @@ describe('Session', () => {
});
});

describe('origin outage mode fallback', () => {
let dispatchSpy: ReturnType<typeof vi.spyOn>;
let fetchSpy: ReturnType<typeof vi.spyOn>;

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({
Expand Down
1 change: 0 additions & 1 deletion packages/shared/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
45 changes: 0 additions & 45 deletions packages/shared/src/errors/missingExpiredTokenError.ts

This file was deleted.

Loading