const DEFAULT_MAX_RETRIES = 3; const MAX_BACKOFF_MS = 8000; function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } function parseRetryAfterMs(value: string | null): number | null { if (!value) return null; const seconds = Number(value); if (Number.isFinite(seconds) && seconds > 0) { return Math.round(seconds * 1000); } return null; } function computeBackoffMs(attempt: number, retryAfterMs: number | null) { if (retryAfterMs && retryAfterMs > 0) { return Math.min(retryAfterMs, MAX_BACKOFF_MS); } const exp = Math.min(MAX_BACKOFF_MS, 500 * 2 ** attempt); const jitter = Math.floor(Math.random() * 300); return exp + jitter; } export async function fetchUploadWithRetry( formData: FormData, options?: { maxRetries?: number; signal?: AbortSignal; } ) { const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES; for (let attempt = 0; ; attempt++) { const response = await fetch("/api/files/upload", { method: "POST", body: formData, signal: options?.signal, }); if (response.status !== 429 || attempt >= maxRetries) { return response; } let retryAfterMs = parseRetryAfterMs(response.headers.get("Retry-After")); if (!retryAfterMs) { try { const payload = await response.clone().json(); const retryAfterSec = Number(payload?.retryAfter || 0); if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) { retryAfterMs = Math.round(retryAfterSec * 1000); } } catch {} } const delayMs = computeBackoffMs(attempt, retryAfterMs); await sleep(delayMs); } }