* Fetch with retry logic for transient errors (502, 503, etc.) * Implements exponential backoff between retries.
(
url: URL | string,
options: RequestInit,
logger?: { warn: (obj: object, msg: string) => void },
)
| 48 | * Implements exponential backoff between retries. |
| 49 | */ |
| 50 | async function fetchWithRetry( |
| 51 | url: URL | string, |
| 52 | options: RequestInit, |
| 53 | logger?: { warn: (obj: object, msg: string) => void }, |
| 54 | ): Promise<Response> { |
| 55 | let lastError: Error | null = null |
| 56 | let backoffDelay = RETRY_BACKOFF_BASE_DELAY_MS |
| 57 | |
| 58 | for (let attempt = 0; attempt <= MAX_RETRIES_PER_MESSAGE; attempt++) { |
| 59 | try { |
| 60 | const response = await fetch(url, options) |
| 61 | |
| 62 | // If response is OK or not retryable, return it |
| 63 | if (response.ok || !isRetryableStatusCode(response.status)) { |
| 64 | return response |
| 65 | } |
| 66 | |
| 67 | // Retryable error - log and continue to retry |
| 68 | if (attempt < MAX_RETRIES_PER_MESSAGE) { |
| 69 | logger?.warn( |
| 70 | { status: response.status, attempt: attempt + 1, url: String(url) }, |
| 71 | `Retryable HTTP error, retrying in ${backoffDelay}ms`, |
| 72 | ) |
| 73 | await new Promise((resolve) => setTimeout(resolve, backoffDelay)) |
| 74 | backoffDelay = Math.min(backoffDelay * 2, RETRY_BACKOFF_MAX_DELAY_MS) |
| 75 | } else { |
| 76 | // Last attempt, return the response even if it's an error |
| 77 | return response |
| 78 | } |
| 79 | } catch (error) { |
| 80 | // Network-level error (DNS, connection refused, etc.) |
| 81 | lastError = error instanceof Error ? error : new Error(String(error)) |
| 82 | |
| 83 | if (attempt < MAX_RETRIES_PER_MESSAGE) { |
| 84 | logger?.warn( |
| 85 | { error: getErrorObject(lastError), attempt: attempt + 1, url: String(url) }, |
| 86 | `Network error, retrying in ${backoffDelay}ms`, |
| 87 | ) |
| 88 | await new Promise((resolve) => setTimeout(resolve, backoffDelay)) |
| 89 | backoffDelay = Math.min(backoffDelay * 2, RETRY_BACKOFF_MAX_DELAY_MS) |
| 90 | } |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | // All retries exhausted - throw the last error |
| 95 | throw lastError ?? new Error('Request failed after retries') |
| 96 | } |
| 97 | |
| 98 | export async function getUserInfoFromApiKey<T extends UserColumn>( |
| 99 | params: GetUserInfoFromApiKeyInput<T>, |
no test coverage detected