diff --git a/.changeset/lazy-items-crash.md b/.changeset/lazy-items-crash.md
new file mode 100644
index 00000000000..bd6378741a8
--- /dev/null
+++ b/.changeset/lazy-items-crash.md
@@ -0,0 +1,5 @@
+---
+'@clerk/shared': minor
+---
+
+Improve error handling and retry logic when loading `@clerk/clerk-js`.
diff --git a/integration/templates/next-app-router/src/app/clerk-status/page.tsx b/integration/templates/next-app-router/src/app/clerk-status/page.tsx
new file mode 100644
index 00000000000..256228b16c8
--- /dev/null
+++ b/integration/templates/next-app-router/src/app/clerk-status/page.tsx
@@ -0,0 +1,35 @@
+'use client';
+import { ClerkLoaded, ClerkLoading, ClerkFailed, ClerkDegraded, useClerk } from '@clerk/nextjs';
+
+export default function ClerkStatusPage() {
+ const { loaded, status } = useClerk();
+
+ return (
+ <>
+
Status: {status}
+ {status === 'loading' ? 'Clerk is loading' : null}
+ {status === 'error' ? 'Clerk is out' : null}
+ {status === 'degraded' ? 'Clerk is degraded' : null}
+ {status === 'ready' ? 'Clerk is ready' : null}
+ {status === 'ready' || status === 'degraded' ? 'Clerk is ready or degraded (loaded)' : null}
+ {loaded ? 'Clerk is loaded' : null}
+ {!loaded ? 'Clerk is NOT loaded' : null}
+
+
+ (comp) Clerk is degraded
+
+
+
+ (comp) Clerk is loaded,(ready or degraded)
+
+
+
+ (comp) Something went wrong with Clerk, refresh your page.
+
+
+
+ (comp) Waiting for clerk to fail, ready or degraded.
+
+ >
+ );
+}
diff --git a/integration/templates/react-vite/src/clerk-status/index.tsx b/integration/templates/react-vite/src/clerk-status/index.tsx
index b8cbfd2c0b0..eaae6b01baf 100644
--- a/integration/templates/react-vite/src/clerk-status/index.tsx
+++ b/integration/templates/react-vite/src/clerk-status/index.tsx
@@ -27,7 +27,7 @@ export default function ClerkStatusPage() {
- (comp) Waiting for clerk to fail, ready or regraded.
+ (comp) Waiting for clerk to fail, ready or degraded.
>
);
diff --git a/integration/tests/resiliency.test.ts b/integration/tests/resiliency.test.ts
index b4d2505ae50..561c03130e0 100644
--- a/integration/tests/resiliency.test.ts
+++ b/integration/tests/resiliency.test.ts
@@ -21,10 +21,6 @@ const make500ClerkResponse = () => ({
testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('resiliency @generic', ({ app }) => {
test.describe.configure({ mode: 'serial' });
- if (app.name.includes('next')) {
- test.skip();
- }
-
let fakeUser: FakeUser;
test.beforeAll(async () => {
@@ -38,221 +34,364 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('resilienc
await app.teardown();
});
- test('signed in users can get a fresh session token when Client fails to load', async ({ page, context }) => {
- const u = createTestUtils({ app, page, context });
+ test.describe('loading resiliency', () => {
+ test('signed in users can get a fresh session token when Client fails to load', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
- await u.po.signIn.goTo();
- await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
- await u.po.expect.toBeSignedIn();
+ await u.po.signIn.goTo();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.po.expect.toBeSignedIn();
- const tokenAfterSignIn = await page.evaluate(() => {
- return window.Clerk?.session?.getToken();
- });
+ const tokenAfterSignIn = await page.evaluate(() => {
+ return window.Clerk?.session?.getToken();
+ });
- // Simulate developer coming back and client fails to load.
- await page.route('**/v1/client?**', route => route.fulfill(make500ClerkResponse()));
+ // Simulate developer coming back and client fails to load.
+ await page.route('**/v1/client?**', route => route.fulfill(make500ClerkResponse()));
- await page.waitForTimeout(1_000);
- await page.reload();
+ await page.waitForTimeout(1_000);
+ await page.reload();
- const waitForClientImmediately = page.waitForResponse(
- response => response.url().includes('/client?') && response.status() === 500,
- { timeout: 3_000 },
- );
+ const waitForClientImmediately = page.waitForResponse(
+ response => response.url().includes('/client?') && response.status() === 500,
+ { timeout: 3_000 },
+ );
- const waitForTokenImmediately = page.waitForResponse(
- response =>
- response.url().includes('/tokens?') && response.status() === 200 && response.request().method() === 'POST',
- { timeout: 3_000 },
- );
+ const waitForTokenImmediately = page.waitForResponse(
+ response =>
+ response.url().includes('/tokens?') && response.status() === 200 && response.request().method() === 'POST',
+ { timeout: 3_000 },
+ );
- await page.waitForLoadState('domcontentloaded');
+ await page.waitForLoadState('domcontentloaded');
- await waitForClientImmediately;
- await waitForTokenImmediately;
+ await waitForClientImmediately;
+ await waitForTokenImmediately;
+
+ // Wait for the client to be loaded. and the internal `getToken({skipCache: true})` to have been completed.
+ await u.po.clerk.toBeLoaded();
+
+ // Read the newly refreshed token.
+ const tokenOnClientOutage = await page.evaluate(() => {
+ return window.Clerk?.session?.getToken();
+ });
- // Wait for the client to be loaded. and the internal `getToken({skipCache: true})` to have been completed.
- await u.po.clerk.toBeLoaded();
+ expect(tokenOnClientOutage).not.toEqual(tokenAfterSignIn);
- // Read the newly refreshed token.
- const tokenOnClientOutage = await page.evaluate(() => {
- return window.Clerk?.session?.getToken();
+ await u.po.expect.toBeSignedIn();
});
- expect(tokenOnClientOutage).not.toEqual(tokenAfterSignIn);
+ test('resiliency to not break devBrowser - dummy client and is not created on `/client` 4xx errors', async ({
+ page,
+ context,
+ }) => {
+ // Simulate "Needs new dev browser, when db jwt exists but does not match the instance".
+
+ const response = {
+ status: 401,
+ body: JSON.stringify({
+ errors: [
+ {
+ message: '',
+ long_message: '',
+ code: 'dev_browser_unauthenticated',
+ },
+ ],
+ clerk_trace_id: 'some-trace-id',
+ }),
+ };
+
+ const u = createTestUtils({ app, page, context, useTestingToken: false });
+
+ await page.route('**/v1/client?**', route => {
+ return route.fulfill(response);
+ });
- await u.po.expect.toBeSignedIn();
- });
+ await page.route('**/v1/environment?**', route => {
+ return route.fulfill(response);
+ });
- test('resiliency to not break devBrowser - dummy client and is not created on `/client` 4xx errors', async ({
- page,
- context,
- }) => {
- // Simulate "Needs new dev browser, when db jwt exists but does not match the instance".
-
- const response = {
- status: 401,
- body: JSON.stringify({
- errors: [
- {
- message: '',
- long_message: '',
- code: 'dev_browser_unauthenticated',
- },
- ],
- clerk_trace_id: 'some-trace-id',
- }),
- };
-
- const u = createTestUtils({ app, page, context, useTestingToken: false });
-
- await page.route('**/v1/client?**', route => {
- return route.fulfill(response);
- });
+ const waitForClientImmediately = page.waitForResponse(
+ response => response.url().includes('/client?') && response.status() === 401,
+ { timeout: 3_000 },
+ );
+
+ const waitForEnvironmentImmediately = page.waitForResponse(
+ response => response.url().includes('/environment?') && response.status() === 401,
+ { timeout: 3_000 },
+ );
+
+ await u.page.goToAppHome();
+ await page.waitForLoadState('domcontentloaded');
+
+ await waitForEnvironmentImmediately;
+ const waitForDevBrowserImmediately = page.waitForResponse(
+ response => response.url().includes('/dev_browser') && response.status() === 200,
+ {
+ timeout: 4_000,
+ },
+ );
+ await waitForClientImmediately;
+
+ // To remove specific route handlers
+ await page.unrouteAll();
+
+ await waitForDevBrowserImmediately;
- await page.route('**/v1/environment?**', route => {
- return route.fulfill(response);
+ await u.po.clerk.toBeLoaded();
});
- const waitForClientImmediately = page.waitForResponse(
- response => response.url().includes('/client?') && response.status() === 401,
- { timeout: 3_000 },
- );
+ test.describe('Clerk.status', () => {
+ test('normal flow shows correct states and transitions', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/clerk-status');
+
+ // Initial state checks
+ await expect(page.getByText('Status: loading', { exact: true })).toBeVisible();
+ await expect(page.getByText('Clerk is loading', { exact: true })).toBeVisible();
+ await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();
+ await expect(page.getByText('Clerk is out')).toBeHidden();
+ await expect(page.getByText('Clerk is degraded')).toBeHidden();
+ await expect(page.getByText('(comp) Waiting for clerk to fail, ready or degraded.')).toBeVisible();
+ await u.po.clerk.toBeLoading();
+
+ // Wait for loading to complete and verify final state
+ await expect(page.getByText('Status: ready', { exact: true })).toBeVisible();
+ await u.po.clerk.toBeLoaded();
+ await u.po.clerk.toBeReady();
+ await expect(page.getByText('Clerk is ready', { exact: true })).toBeVisible();
+ await expect(page.getByText('Clerk is ready or degraded (loaded)')).toBeVisible();
+ await expect(page.getByText('Clerk is loaded', { exact: true })).toBeVisible();
+ await expect(page.getByText('(comp) Clerk is loaded,(ready or degraded)')).toBeVisible();
+
+ // Verify loading component is no longer visible
+ await expect(page.getByText('(comp) Waiting for clerk to fail, ready or degraded.')).toBeHidden();
+ });
- const waitForEnvironmentImmediately = page.waitForResponse(
- response => response.url().includes('/environment?') && response.status() === 401,
- { timeout: 3_000 },
- );
+ test('clerk-js hotloading failed', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
- await u.page.goToAppHome();
- await page.waitForLoadState('domcontentloaded');
+ await page.route('**/clerk.browser.js', route => route.abort());
- await waitForEnvironmentImmediately;
- const waitForDevBrowserImmediately = page.waitForResponse(
- response => response.url().includes('/dev_browser') && response.status() === 200,
- {
- timeout: 4_000,
- },
- );
- await waitForClientImmediately;
+ await u.page.goToRelative('/clerk-status');
+
+ // Initial state checks
+ await expect(page.getByText('Status: loading', { exact: true })).toBeVisible();
+ await expect(page.getByText('Clerk is loading', { exact: true })).toBeVisible();
+ await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();
- // To remove specific route handlers
- await page.unrouteAll();
+ // Wait for loading to complete and verify final state
+ // Account for the new 15-second script loading timeout plus buffer for UI updates
+ await expect(page.getByText('Status: error', { exact: true })).toBeVisible({
+ timeout: 16_000,
+ });
+ await expect(page.getByText('Clerk is out', { exact: true })).toBeVisible();
+ await expect(page.getByText('Clerk is ready or degraded (loaded)')).toBeHidden();
+ await expect(page.getByText('Clerk is loaded', { exact: true })).toBeHidden();
+ await expect(page.getByText('(comp) Clerk is loaded,(ready or degraded)')).toBeHidden();
+ await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();
+
+ // Verify loading component is no longer visible
+ await expect(page.getByText('Clerk is loading', { exact: true })).toBeHidden();
+ });
+
+ test('clerk-js client fails and status degraded', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ await page.route('**/v1/client?**', route => route.fulfill(make500ClerkResponse()));
+
+ await u.page.goToRelative('/clerk-status');
+
+ // Initial state checks
+ await expect(page.getByText('Status: loading', { exact: true })).toBeVisible();
+ await expect(page.getByText('Clerk is loading', { exact: true })).toBeVisible();
+ await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();
+
+ // Wait for loading to complete and verify final state
+ await expect(page.getByText('Status: degraded', { exact: true })).toBeVisible({
+ timeout: 10_000,
+ });
+ await u.po.clerk.toBeDegraded();
+ await expect(page.getByText('Clerk is degraded', { exact: true })).toBeVisible();
+ await expect(page.getByText('Clerk is ready', { exact: true })).toBeHidden();
+ await expect(page.getByText('Clerk is ready or degraded (loaded)')).toBeVisible();
+ await expect(page.getByText('Clerk is loaded', { exact: true })).toBeVisible();
+ await expect(page.getByText('(comp) Clerk is loaded,(ready or degraded)')).toBeVisible();
+ await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeHidden();
+ await expect(page.getByText('(comp) Clerk is degraded')).toBeVisible();
+
+ // Verify loading component is no longer visible
+ await expect(page.getByText('Clerk is loading', { exact: true })).toBeHidden();
+ });
- await waitForDevBrowserImmediately;
+ test('clerk-js environment fails and status degraded', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
- await u.po.clerk.toBeLoaded();
+ await page.route('**/v1/environment?**', route => route.fulfill(make500ClerkResponse()));
+
+ await u.page.goToRelative('/clerk-status');
+
+ // Initial state checks
+ await expect(page.getByText('Status: loading', { exact: true })).toBeVisible();
+ await expect(page.getByText('Clerk is loading', { exact: true })).toBeVisible();
+ await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();
+ await u.po.clerk.toBeLoading();
+
+ // Wait for loading to complete and verify final state
+ await expect(page.getByText('Status: degraded', { exact: true })).toBeVisible();
+ await u.po.clerk.toBeDegraded();
+ await expect(page.getByText('Clerk is degraded', { exact: true })).toBeVisible();
+ await expect(page.getByText('Clerk is ready', { exact: true })).toBeHidden();
+ await expect(page.getByText('Clerk is ready or degraded (loaded)')).toBeVisible();
+ await expect(page.getByText('Clerk is loaded', { exact: true })).toBeVisible();
+ await expect(page.getByText('(comp) Clerk is loaded,(ready or degraded)')).toBeVisible();
+ await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeHidden();
+ await expect(page.getByText('(comp) Clerk is degraded')).toBeVisible();
+
+ // Verify loading component is no longer visible
+ await expect(page.getByText('Clerk is loading', { exact: true })).toBeHidden();
+ });
+ });
});
- test.describe('Clerk.status', () => {
- test('normal flow shows correct states and transitions', async ({ page, context }) => {
+ test.describe('clerk-js script loading', () => {
+ test('recovers from transient network failure on clerk-js script load', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
+
+ let requestCount = 0;
+ await page.route('**/clerk.browser.js', route => {
+ requestCount++;
+ // Fail the first request, allow subsequent requests
+ if (requestCount === 1) {
+ return route.abort('failed');
+ }
+ return route.continue();
+ });
+
await u.page.goToRelative('/clerk-status');
- // Initial state checks
+ // Initial state should show loading
await expect(page.getByText('Status: loading', { exact: true })).toBeVisible();
- await expect(page.getByText('Clerk is loading', { exact: true })).toBeVisible();
- await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();
- await expect(page.getByText('Clerk is out')).toBeHidden();
- await expect(page.getByText('Clerk is degraded')).toBeHidden();
- await expect(page.getByText('(comp) Waiting for clerk to fail, ready or regraded.')).toBeVisible();
- await u.po.clerk.toBeLoading();
- // Wait for loading to complete and verify final state
- await expect(page.getByText('Status: ready', { exact: true })).toBeVisible();
+ // Wait for Clerk to eventually load after retry
+ // Account for retry delay + script load time + initialization
+ await expect(page.getByText('Status: ready', { exact: true })).toBeVisible({
+ timeout: 20_000,
+ });
+
await u.po.clerk.toBeLoaded();
- await u.po.clerk.toBeReady();
- await expect(page.getByText('Clerk is ready', { exact: true })).toBeVisible();
- await expect(page.getByText('Clerk is ready or degraded (loaded)')).toBeVisible();
await expect(page.getByText('Clerk is loaded', { exact: true })).toBeVisible();
- await expect(page.getByText('(comp) Clerk is loaded,(ready or degraded)')).toBeVisible();
- // Verify loading component is no longer visible
- await expect(page.getByText('(comp) Waiting for clerk to fail, ready or regraded.')).toBeHidden();
+ // Verify retry happened
+ expect(requestCount).toBeGreaterThan(1);
});
- test('clerk-js hotloading failed', async ({ page, context }) => {
+ test('recovers from HTTP 500 error on clerk-js script load', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
- await page.route('**/clerk.browser.js', route => route.abort());
+ let requestCount = 0;
+ await page.route('**/clerk.browser.js', route => {
+ requestCount++;
+ // Return 500 error on first request, succeed on subsequent
+ if (requestCount === 1) {
+ return route.fulfill({
+ status: 500,
+ body: 'Internal Server Error',
+ });
+ }
+ return route.continue();
+ });
await u.page.goToRelative('/clerk-status');
- // Initial state checks
+ // Initial state should show loading
await expect(page.getByText('Status: loading', { exact: true })).toBeVisible();
- await expect(page.getByText('Clerk is loading', { exact: true })).toBeVisible();
- await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();
- // Wait for loading to complete and verify final state
- // Account for the new 15-second script loading timeout plus buffer for UI updates
- await expect(page.getByText('Status: error', { exact: true })).toBeVisible({
- timeout: 16_000,
+ // Wait for Clerk to eventually load after retry
+ await expect(page.getByText('Status: ready', { exact: true })).toBeVisible({
+ timeout: 20_000,
});
- await expect(page.getByText('Clerk is out', { exact: true })).toBeVisible();
- await expect(page.getByText('Clerk is ready or degraded (loaded)')).toBeHidden();
- await expect(page.getByText('Clerk is loaded', { exact: true })).toBeHidden();
- await expect(page.getByText('(comp) Clerk is loaded,(ready or degraded)')).toBeHidden();
- await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();
- // Verify loading component is no longer visible
- await expect(page.getByText('Clerk is loading', { exact: true })).toBeHidden();
+ await u.po.clerk.toBeLoaded();
+
+ // Verify retry happened
+ expect(requestCount).toBeGreaterThan(1);
});
- test('clerk-js client fails and status degraded', async ({ page, context }) => {
+ test('recovers from HTTP 503 service unavailable with retry', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
- await page.route('**/v1/client?**', route => route.fulfill(make500ClerkResponse()));
+ let requestCount = 0;
+ await page.route('**/clerk.browser.js', route => {
+ requestCount++;
+ // Return 503 error on first two requests, succeed on third
+ if (requestCount <= 2) {
+ return route.fulfill({
+ status: 503,
+ body: 'Service Unavailable',
+ });
+ }
+ return route.continue();
+ });
await u.page.goToRelative('/clerk-status');
- // Initial state checks
- await expect(page.getByText('Status: loading', { exact: true })).toBeVisible();
- await expect(page.getByText('Clerk is loading', { exact: true })).toBeVisible();
- await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();
-
- // Wait for loading to complete and verify final state
- await expect(page.getByText('Status: degraded', { exact: true })).toBeVisible({
- timeout: 10_000,
+ // Wait for Clerk to eventually load after multiple retries
+ await expect(page.getByText('Status: ready', { exact: true })).toBeVisible({
+ timeout: 25_000,
});
- await u.po.clerk.toBeDegraded();
- await expect(page.getByText('Clerk is degraded', { exact: true })).toBeVisible();
- await expect(page.getByText('Clerk is ready', { exact: true })).toBeHidden();
- await expect(page.getByText('Clerk is ready or degraded (loaded)')).toBeVisible();
- await expect(page.getByText('Clerk is loaded', { exact: true })).toBeVisible();
- await expect(page.getByText('(comp) Clerk is loaded,(ready or degraded)')).toBeVisible();
- await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeHidden();
- await expect(page.getByText('(comp) Clerk is degraded')).toBeVisible();
- // Verify loading component is no longer visible
- await expect(page.getByText('Clerk is loading', { exact: true })).toBeHidden();
+ await u.po.clerk.toBeLoaded();
+
+ // Verify multiple retries happened
+ expect(requestCount).toBeGreaterThan(2);
});
- test('clerk-js environment fails and status degraded', async ({ page, context }) => {
+ test('fails with error status after exhausting all retries', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
- await page.route('**/v1/environment?**', route => route.fulfill(make500ClerkResponse()));
+ // Block all clerk.browser.js requests permanently
+ await page.route('**/clerk.browser.js', route => route.abort('failed'));
await u.page.goToRelative('/clerk-status');
- // Initial state checks
+ // Initial state should show loading
await expect(page.getByText('Status: loading', { exact: true })).toBeVisible();
- await expect(page.getByText('Clerk is loading', { exact: true })).toBeVisible();
+
+ // Wait for error status after all retries are exhausted
+ // This should take longer due to exponential backoff
+ await expect(page.getByText('Status: error', { exact: true })).toBeVisible({
+ timeout: 30_000,
+ });
+
+ await expect(page.getByText('Clerk is out', { exact: true })).toBeVisible();
await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();
- await u.po.clerk.toBeLoading();
-
- // Wait for loading to complete and verify final state
- await expect(page.getByText('Status: degraded', { exact: true })).toBeVisible();
- await u.po.clerk.toBeDegraded();
- await expect(page.getByText('Clerk is degraded', { exact: true })).toBeVisible();
- await expect(page.getByText('Clerk is ready', { exact: true })).toBeHidden();
- await expect(page.getByText('Clerk is ready or degraded (loaded)')).toBeVisible();
- await expect(page.getByText('Clerk is loaded', { exact: true })).toBeVisible();
- await expect(page.getByText('(comp) Clerk is loaded,(ready or degraded)')).toBeVisible();
- await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeHidden();
- await expect(page.getByText('(comp) Clerk is degraded')).toBeVisible();
+ });
- // Verify loading component is no longer visible
- await expect(page.getByText('Clerk is loading', { exact: true })).toBeHidden();
+ test('handles slow network with eventual success', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ let requestCount = 0;
+ await page.route('**/clerk.browser.js', async route => {
+ requestCount++;
+ // First request times out (simulate by very long delay)
+ if (requestCount === 1) {
+ // Wait longer than typical timeout, then abort
+ await new Promise(resolve => setTimeout(resolve, 3000));
+ return route.abort('timedout');
+ }
+ // Second request succeeds normally
+ return route.continue();
+ });
+
+ await u.page.goToRelative('/clerk-status');
+
+ // Wait for Clerk to eventually load
+ await expect(page.getByText('Status: ready', { exact: true })).toBeVisible({
+ timeout: 25_000,
+ });
+
+ await u.po.clerk.toBeLoaded();
});
});
});
diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts
index ff4283d9ad9..67ebead980f 100644
--- a/packages/shared/src/loadClerkJsScript.ts
+++ b/packages/shared/src/loadClerkJsScript.ts
@@ -60,14 +60,63 @@ function isClerkProperlyLoaded(): boolean {
return typeof clerk === 'object' && typeof clerk.load === 'function';
}
+/**
+ * Checks if an existing script has a request error using Performance API.
+ *
+ * @param scriptUrl - The URL of the script to check.
+ * @returns True if the script has failed to load due to a network/HTTP error.
+ */
+function hasScriptRequestError(scriptUrl: string): boolean {
+ if (typeof window === 'undefined' || !window.performance) {
+ return false;
+ }
+
+ const entries = performance.getEntriesByName(scriptUrl, 'resource') as PerformanceResourceTiming[];
+
+ if (entries.length === 0) {
+ return false;
+ }
+
+ const scriptEntry = entries[entries.length - 1];
+
+ // transferSize === 0 with responseEnd === 0 indicates network failure
+ // transferSize === 0 with responseEnd > 0 might be a 4xx/5xx error or blocked request
+ if (scriptEntry.transferSize === 0 && scriptEntry.decodedBodySize === 0) {
+ // If there was no response at all, it's definitely an error
+ if (scriptEntry.responseEnd === 0) {
+ return true;
+ }
+ // If we got a response but no content, likely an HTTP error (4xx/5xx)
+ if (scriptEntry.responseEnd > 0 && scriptEntry.responseStart > 0) {
+ return true;
+ }
+
+ if ('responseStatus' in scriptEntry) {
+ const status = (scriptEntry as any).responseStatus;
+ if (status >= 400) {
+ return true;
+ }
+ if (scriptEntry.responseStatus === 0) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
/**
* Waits for Clerk to be properly loaded with a timeout mechanism.
* Uses polling to check if Clerk becomes available within the specified timeout.
*
* @param timeoutMs - Maximum time to wait in milliseconds.
+ * @param existingScript - The existing script element to wait for. Optional, for existing scripts.
* @returns Promise that resolves with null if Clerk loads successfully, or rejects with an error if timeout is reached.
*/
-function waitForClerkWithTimeout(timeoutMs: number): Promise {
+function waitForClerkWithTimeout(
+ timeoutMs: number,
+ existingScript?: HTMLScriptElement,
+): Promise {
return new Promise((resolve, reject) => {
let resolved = false;
@@ -76,6 +125,12 @@ function waitForClerkWithTimeout(timeoutMs: number): Promise {
+ cleanup(timeoutId, pollInterval);
+ reject(new ClerkRuntimeError(FAILED_TO_LOAD_ERROR, { code: ERROR_CODE }));
+ });
+
const checkAndResolve = () => {
if (resolved) {
return;
@@ -118,11 +173,13 @@ function waitForClerkWithTimeout(timeoutMs: number): Promise('script[data-clerk-js-script]');
-
- if (existingScript) {
- return waitForClerkWithTimeout(timeout);
- }
-
if (!opts?.publishableKey) {
errorThrower.throwMissingPublishableKeyError();
return null;
}
+ const scriptUrl = clerkJsScriptUrl(opts);
+ const existingScript = document.querySelector('script[data-clerk-js-script]');
+
+ if (existingScript) {
+ if (hasScriptRequestError(scriptUrl)) {
+ existingScript.remove();
+ } else {
+ try {
+ await waitForClerkWithTimeout(timeout, existingScript);
+ return null;
+ } catch {
+ existingScript.remove();
+ }
+ }
+ }
+
const loadPromise = waitForClerkWithTimeout(timeout);
- loadScript(clerkJsScriptUrl(opts), {
+ loadScript(scriptUrl, {
async: true,
crossOrigin: 'anonymous',
nonce: opts.nonce,