feat: retry con backoff su errori transitori + timeoutMs applicato
- gatewayRequest: max 3 retry su 429/5xx/timeout/errore rete, backoff esponenziale + jitter, rispetta Retry-After (cap 10s) - AbortError (annullamento utente) propagato, mai ritentato - timeoutMs dalla config usato come fallback quando pi non fornisce signal - testato: 503→200, sempre-503 (4 tentativi), 429+Retry-After, rete giù, abort
This commit is contained in:
+43
-8
@@ -37,6 +37,14 @@ const CONFIG_DEFAULTS: MemoryConfig = {
|
||||
timeoutMs: 30_000,
|
||||
};
|
||||
|
||||
// Retry su errori transitori (429/5xx/timeout): backoff esponenziale + jitter
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_BASE_MS = 500;
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
function loadConfig(): MemoryConfig {
|
||||
try {
|
||||
return { ...CONFIG_DEFAULTS, ...JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")) };
|
||||
@@ -67,14 +75,41 @@ async function gatewayRequest(
|
||||
"X-API-Key": cfg.apiKey,
|
||||
};
|
||||
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
|
||||
const res = await fetch(`${cfg.url}${route}`, {
|
||||
method,
|
||||
signal,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return { ok: res.ok, status: res.status, data };
|
||||
|
||||
let lastError: unknown = null;
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
// timeout fallback: se pi non fornisce un signal, usa timeoutMs dalla config
|
||||
const timeoutSignal = signal ?? AbortSignal.timeout(cfg.timeoutMs ?? 30_000);
|
||||
try {
|
||||
const res = await fetch(`${cfg.url}${route}`, {
|
||||
method,
|
||||
signal: timeoutSignal,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
// retry solo su errori transitori (429/5xx), rispettando Retry-After
|
||||
if ((res.status === 429 || res.status >= 500) && attempt < MAX_RETRIES) {
|
||||
const retryAfter = res.headers.get("retry-after");
|
||||
const delay = retryAfter
|
||||
? Math.min(Number(retryAfter) * 1000 || RETRY_BASE_MS, 10_000)
|
||||
: RETRY_BASE_MS * 2 ** attempt + Math.random() * 200;
|
||||
await sleep(delay);
|
||||
continue;
|
||||
}
|
||||
return { ok: res.ok, status: res.status, data };
|
||||
} catch (e) {
|
||||
// annullamento utente: propaga, non ritentare
|
||||
if (e instanceof Error && e.name === "AbortError") throw e;
|
||||
// errore di rete/timeout: retry con backoff
|
||||
lastError = e;
|
||||
if (attempt < MAX_RETRIES) {
|
||||
await sleep(RETRY_BASE_MS * 2 ** attempt + Math.random() * 200);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ok: false, status: 0, data: { error: "network_error", detail: String(lastError ?? "unknown") } };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user