ai-chatbot/lib/client/upload-with-retry.ts
Your Name 60349a3bb0
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix(upload): isolate rate limit and add 429 retry backoff
2026-03-05 16:14:38 +08:00

61 lines
1.6 KiB
TypeScript

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);
}
}