| | |
| | |
| | |
| | |
| |
|
| | export interface FetchWithRetryOptions { |
| | retries?: number |
| | retryDelay?: number |
| | timeout?: number |
| | throwHttpErrors?: boolean |
| | |
| | |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | function calculateDefaultDelay(attempt: number): number { |
| | return 1000 * Math.pow(2, attempt - 1) + Math.random() * 100 |
| | } |
| |
|
| | |
| | |
| | |
| | function sleep(ms: number): Promise<void> { |
| | return new Promise((resolve) => setTimeout(resolve, ms)) |
| | } |
| |
|
| | |
| | |
| | |
| | async function fetchWithTimeout( |
| | url: string | URL, |
| | init?: RequestInit, |
| | timeout?: number, |
| | ): Promise<Response> { |
| | if (!timeout) { |
| | return fetch(url, init) |
| | } |
| |
|
| | const controller = new AbortController() |
| | const timeoutId = setTimeout(() => controller.abort(), timeout) |
| |
|
| | try { |
| | const response = await fetch(url, { |
| | ...init, |
| | signal: controller.signal, |
| | }) |
| | clearTimeout(timeoutId) |
| | return response |
| | } catch (error) { |
| | clearTimeout(timeoutId) |
| | if (error instanceof Error && error.name === 'AbortError') { |
| | throw new Error(`Request timed out after ${timeout}ms`) |
| | } |
| | throw error |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | export async function fetchWithRetry( |
| | url: string | URL, |
| | init?: RequestInit, |
| | options: FetchWithRetryOptions = {}, |
| | ): Promise<Response> { |
| | const { retries = 0, timeout, throwHttpErrors = true } = options |
| |
|
| | let lastError: Error | null = null |
| |
|
| | for (let attempt = 0; attempt <= retries; attempt++) { |
| | try { |
| | const response = await fetchWithTimeout(url, init, timeout) |
| |
|
| | |
| | if (response.status >= 500 && attempt < retries) { |
| | lastError = new Error(`HTTP ${response.status}: ${response.statusText}`) |
| | const delay = calculateDefaultDelay(attempt + 1) |
| | await sleep(delay) |
| | continue |
| | } |
| |
|
| | |
| | if (throwHttpErrors && !response.ok && response.status >= 400) { |
| | throw new Error(`HTTP ${response.status}: ${response.statusText}`) |
| | } |
| |
|
| | return response |
| | } catch (error) { |
| | lastError = error instanceof Error ? error : new Error(String(error)) |
| |
|
| | |
| | if (attempt === retries) { |
| | throw lastError |
| | } |
| |
|
| | |
| | if ( |
| | error instanceof Error && |
| | error.message.includes('HTTP 4') && |
| | !error.message.includes('HTTP 429') |
| | ) { |
| | throw lastError |
| | } |
| |
|
| | |
| | const delay = calculateDefaultDelay(attempt + 1) |
| | await sleep(delay) |
| | } |
| | } |
| |
|
| | throw lastError || new Error('Maximum retries exceeded') |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | export async function fetchStream( |
| | url: string | URL, |
| | init?: RequestInit, |
| | options: FetchWithRetryOptions = {}, |
| | ): Promise<Response> { |
| | const { timeout, throwHttpErrors = true } = options |
| |
|
| | const response = await fetchWithTimeout(url, init, timeout) |
| |
|
| | |
| | if (throwHttpErrors && !response.ok && response.status >= 400) { |
| | throw new Error(`HTTP ${response.status}: ${response.statusText}`) |
| | } |
| |
|
| | return response |
| | } |
| |
|