Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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