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 2b2fe211903..de07b73658e 100644 --- a/packages/testing/src/common/setup.ts +++ b/packages/testing/src/common/setup.ts @@ -1,9 +1,38 @@ 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 = + typeof error.retryAfter === 'number' + ? Math.min(error.retryAfter * 1000, MAX_RETRY_DELAY_MS) + : Math.min(BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * JITTER_MAX_MS, MAX_RETRY_DELAY_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 +73,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.');