From 65b0620684e541196550ba37e6857265ce028da7 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 19 Mar 2026 22:54:46 -0500 Subject: [PATCH 1/3] fix(testing): add retry logic for testing token fetch on 429/5xx --- packages/testing/src/common/setup.ts | 32 +++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/testing/src/common/setup.ts b/packages/testing/src/common/setup.ts index 2b2fe211903..a9575ea6820 100644 --- a/packages/testing/src/common/setup.ts +++ b/packages/testing/src/common/setup.ts @@ -1,9 +1,36 @@ import { createClerkClient } from '@clerk/backend'; +import { isClerkAPIResponseError } from '@clerk/shared/error'; import { parsePublishableKey } from '@clerk/shared/keys'; import dotenv from 'dotenv'; import type { ClerkSetupOptions, ClerkSetupReturn } from './types'; +const MAX_RETRIES = 5; +const BASE_DELAY_MS = 1000; +const JITTER_MAX_MS = 500; +const MAX_RETRY_DELAY_MS = 30_000; +const RETRYABLE_STATUS_CODES = new Set([429, 502, 503, 504]); + +async function fetchWithRetry(fn: () => Promise, label: string): Promise { + for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { + try { + return await fn(); + } catch (error) { + const isRetryable = isClerkAPIResponseError(error) && RETRYABLE_STATUS_CODES.has(error.status); + if (!isRetryable || attempt === MAX_RETRIES) { + throw error; + } + const delay = + isClerkAPIResponseError(error) && typeof error.retryAfter === 'number' + ? Math.min(error.retryAfter * 1000, MAX_RETRY_DELAY_MS) + : BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * JITTER_MAX_MS; + console.warn(`[Retry] ${error.status} for ${label}, attempt ${attempt + 1}/${MAX_RETRIES}, waiting ${Math.round(delay)}ms`); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + throw new Error('Unreachable'); +} + export const fetchEnvVars = async (options?: ClerkSetupOptions): Promise => { const { debug = false, dotenv: loadDotEnv = true, ...rest } = options || {}; @@ -44,7 +71,10 @@ export const fetchEnvVars = async (options?: ClerkSetupOptions): Promise clerkClient.testingTokens.createTestingToken(), + 'testingTokens.createTestingToken', + ); testingToken = tokenData.token; } catch (err) { console.error('Failed to fetch testing token from Clerk API.'); From c23b2e4e60c88b9591f9f0f9d50fc888d129b0e8 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 26 Mar 2026 11:25:04 -0500 Subject: [PATCH 2/3] fix(testing): add changeset and cap exponential backoff delay --- .changeset/retry-testing-token.md | 5 +++++ packages/testing/src/common/setup.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/retry-testing-token.md diff --git a/.changeset/retry-testing-token.md b/.changeset/retry-testing-token.md new file mode 100644 index 00000000000..64fb7eaf3c7 --- /dev/null +++ b/.changeset/retry-testing-token.md @@ -0,0 +1,5 @@ +--- +"@clerk/testing": patch +--- + +Add retry logic with exponential backoff for testing token fetch on 429 and 5xx responses. diff --git a/packages/testing/src/common/setup.ts b/packages/testing/src/common/setup.ts index a9575ea6820..fb8303adb93 100644 --- a/packages/testing/src/common/setup.ts +++ b/packages/testing/src/common/setup.ts @@ -21,9 +21,9 @@ async function fetchWithRetry(fn: () => Promise, label: string): Promise setTimeout(resolve, delay)); } From 131cff51bfe119123d708bc324c664312c95f499 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 26 Mar 2026 11:38:13 -0500 Subject: [PATCH 3/3] style(testing): format setup.ts with prettier --- packages/testing/src/common/setup.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/testing/src/common/setup.ts b/packages/testing/src/common/setup.ts index fb8303adb93..de07b73658e 100644 --- a/packages/testing/src/common/setup.ts +++ b/packages/testing/src/common/setup.ts @@ -24,7 +24,9 @@ async function fetchWithRetry(fn: () => Promise, label: string): Promise setTimeout(resolve, delay)); } }