Skip to content

Commit bbd948a

Browse files
committed
fix(clerk-js): Correct init retry logic
1 parent f47b5a3 commit bbd948a

File tree

3 files changed

+177
-13
lines changed

3 files changed

+177
-13
lines changed

packages/clerk-js/src/core/__tests__/clerk.test.ts

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EmailLinkErrorCodeStatus } from '@clerk/shared/error';
1+
import { ClerkRuntimeError, EmailLinkErrorCodeStatus } from '@clerk/shared/error';
22
import type {
33
ActiveSessionResource,
44
PendingSessionResource,
@@ -13,8 +13,10 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, test,
1313
import { mockJwt } from '@/test/core-fixtures';
1414

1515
import { mockNativeRuntime } from '../../test/utils';
16+
import { AuthCookieService } from '../auth/AuthCookieService';
1617
import type { DevBrowser } from '../auth/devBrowser';
1718
import { Clerk } from '../clerk';
19+
import * as errorsModule from '../errors';
1820
import { eventBus, events } from '../events';
1921
import type { DisplayConfig, Organization } from '../resources/internal';
2022
import { BaseResource, Client, Environment, SignIn, SignUp } from '../resources/internal';
@@ -157,6 +159,118 @@ describe('Clerk singleton', () => {
157159
});
158160
});
159161

162+
describe('load retry behavior', () => {
163+
let originalMountComponentRenderer: typeof Clerk.mountComponentRenderer;
164+
165+
const createMockAuthService = () => ({
166+
decorateUrlWithDevBrowserToken: vi.fn((url: URL) => url),
167+
getSessionCookie: vi.fn(() => null),
168+
handleUnauthenticatedDevBrowser: vi.fn(() => Promise.resolve()),
169+
isSignedOut: vi.fn(() => false),
170+
setClientUatCookieForDevelopmentInstances: vi.fn(),
171+
startPollingForToken: vi.fn(),
172+
stopPollingForToken: vi.fn(),
173+
});
174+
175+
const createMockComponentControls = () => {
176+
const componentInstance = {
177+
mountImpersonationFab: vi.fn(),
178+
updateProps: vi.fn(),
179+
};
180+
181+
return {
182+
ensureMounted: vi.fn().mockResolvedValue(componentInstance),
183+
prioritizedOn: vi.fn(),
184+
};
185+
};
186+
187+
beforeEach(() => {
188+
originalMountComponentRenderer = Clerk.mountComponentRenderer;
189+
});
190+
191+
afterEach(() => {
192+
Clerk.mountComponentRenderer = originalMountComponentRenderer;
193+
vi.useRealTimers();
194+
});
195+
196+
it('retries once when dev browser authentication is lost', async () => {
197+
vi.useFakeTimers();
198+
199+
const mockAuthService = createMockAuthService();
200+
const authCreateSpy = vi
201+
.spyOn(AuthCookieService, 'create')
202+
.mockResolvedValue(mockAuthService as unknown as AuthCookieService);
203+
204+
const componentControls = createMockComponentControls();
205+
const devBrowserError = Object.assign(new Error('dev browser unauthenticated'), {
206+
errors: [{ code: 'dev_browser_unauthenticated' }],
207+
status: 401,
208+
});
209+
210+
const mountSpy = vi
211+
.fn<NonNullable<typeof Clerk.mountComponentRenderer>>()
212+
.mockImplementationOnce(() => {
213+
throw devBrowserError;
214+
})
215+
.mockReturnValue(componentControls);
216+
217+
Clerk.mountComponentRenderer = mountSpy;
218+
mockClientFetch.mockClear();
219+
220+
const sut = new Clerk(productionPublishableKey);
221+
222+
try {
223+
const loadPromise = sut.load();
224+
225+
await vi.advanceTimersByTimeAsync(2000);
226+
await loadPromise;
227+
} finally {
228+
authCreateSpy.mockRestore();
229+
}
230+
231+
expect(mountSpy).toHaveBeenCalledTimes(2);
232+
expect(mockAuthService.handleUnauthenticatedDevBrowser).toHaveBeenCalledTimes(1);
233+
expect(mockClientFetch).toHaveBeenCalledTimes(2);
234+
});
235+
236+
it('surfaces network errors after exhausting retries', async () => {
237+
vi.useFakeTimers();
238+
239+
const mockAuthService = createMockAuthService();
240+
const authCreateSpy = vi
241+
.spyOn(AuthCookieService, 'create')
242+
.mockResolvedValue(mockAuthService as unknown as AuthCookieService);
243+
244+
const networkError = new ClerkRuntimeError('Network failure', { code: 'network_error' });
245+
const mountSpy = vi.fn<NonNullable<typeof Clerk.mountComponentRenderer>>().mockImplementation(() => {
246+
throw networkError;
247+
});
248+
249+
Clerk.mountComponentRenderer = mountSpy;
250+
mockClientFetch.mockClear();
251+
252+
const errorSpy = vi.spyOn(errorsModule, 'clerkErrorInitFailed');
253+
const sut = new Clerk(productionPublishableKey);
254+
255+
try {
256+
const loadPromise = sut.load();
257+
const expectationPromise = expect(loadPromise).rejects.toThrow(/Clerk: Network failure/);
258+
259+
await vi.advanceTimersByTimeAsync(2000);
260+
await expectationPromise;
261+
262+
expect(mountSpy).toHaveBeenCalledTimes(2);
263+
expect(mockClientFetch).toHaveBeenCalledTimes(2);
264+
expect(errorSpy).toHaveBeenCalledTimes(1);
265+
expect(errorSpy).toHaveBeenLastCalledWith(networkError);
266+
expect(mockAuthService.handleUnauthenticatedDevBrowser).not.toHaveBeenCalled();
267+
} finally {
268+
authCreateSpy.mockRestore();
269+
errorSpy.mockRestore();
270+
}
271+
});
272+
});
273+
160274
describe('.setActive', () => {
161275
describe('with `active` session status', () => {
162276
const mockSession = {

packages/clerk-js/src/core/clerk.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2570,10 +2570,11 @@ export class Clerk implements ClerkInterface {
25702570

25712571
let initializationDegradedCounter = 0;
25722572

2573-
let retries = 0;
2574-
while (retries < 2) {
2575-
retries++;
2573+
const wait = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));
25762574

2575+
const maxRetries = 2;
2576+
let retries = 0;
2577+
while (retries < maxRetries) {
25772578
try {
25782579
const initEnvironmentPromise = Environment.getInstance()
25792580
.fetch({ touch: shouldTouchEnv })
@@ -2663,18 +2664,29 @@ export class Clerk implements ClerkInterface {
26632664

26642665
break;
26652666
} catch (err) {
2666-
if (isError(err, 'dev_browser_unauthenticated')) {
2667-
await this.#authService.handleUnauthenticatedDevBrowser();
2668-
} else if (!isValidBrowserOnline()) {
2667+
if (!isValidBrowserOnline()) {
26692668
console.warn(err);
26702669
return;
2671-
} else {
2670+
}
2671+
2672+
const isDevBrowserUnauthenticated = isError(err, 'dev_browser_unauthenticated');
2673+
const isNetworkError = isClerkRuntimeError(err) && err.code === 'network_error';
2674+
2675+
if (!isDevBrowserUnauthenticated && !isNetworkError) {
26722676
throw err;
26732677
}
2674-
}
26752678

2676-
if (retries >= 2) {
2677-
clerkErrorInitFailed();
2679+
retries += 1;
2680+
2681+
if (isDevBrowserUnauthenticated) {
2682+
await this.#authService.handleUnauthenticatedDevBrowser();
2683+
}
2684+
2685+
if (retries >= maxRetries) {
2686+
clerkErrorInitFailed(err);
2687+
}
2688+
2689+
await wait(Math.pow(2, retries) * 1_000);
26782690
}
26792691
}
26802692

packages/clerk-js/src/core/errors.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,46 @@ export function clerkNetworkError(url: string, e: Error): never {
1616
throw new Error(`${errorPrefix} Network error at "${url}" - ${e}. Please try again.`);
1717
}
1818

19-
export function clerkErrorInitFailed(): never {
20-
throw new Error(`${errorPrefix} Something went wrong initializing Clerk.`);
19+
const formatErrorCause = (cause: unknown): string | null => {
20+
if (!cause) {
21+
return null;
22+
}
23+
24+
if (cause instanceof Error) {
25+
return cause.message;
26+
}
27+
28+
if (typeof cause === 'string') {
29+
return cause;
30+
}
31+
32+
if (typeof cause === 'number' || typeof cause === 'bigint' || typeof cause === 'boolean') {
33+
return cause.toString();
34+
}
35+
36+
if (typeof cause === 'symbol') {
37+
return cause.description ? `Symbol(${cause.description})` : 'Symbol()';
38+
}
39+
40+
if (typeof cause === 'function') {
41+
return cause.name ? `[Function ${cause.name}]` : '[Function anonymous]';
42+
}
43+
44+
if (typeof cause === 'object') {
45+
try {
46+
return JSON.stringify(cause);
47+
} catch {
48+
return Object.prototype.toString.call(cause);
49+
}
50+
}
51+
52+
return '[Unknown cause]';
53+
};
54+
55+
export function clerkErrorInitFailed(error?: unknown): never {
56+
const formattedCause = formatErrorCause(error);
57+
const cause = formattedCause ? ` Cause: ${formattedCause}` : '';
58+
throw new Error(`${errorPrefix} Something went wrong initializing Clerk.${cause}`);
2159
}
2260

2361
export function clerkErrorDevInitFailed(msg = ''): never {

0 commit comments

Comments
 (0)