File size: 2,777 Bytes
dfe11f8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | /**
* Cliente HTTP reutilizable con retry y timeout.
*
* Responsabilidades:
* - Realizar peticiones GET y POST usando fetch nativo de Node.js.
* - Aplicar timeout via AbortController.
* - Reintentar automaticamente ante errores de red o HTTP 429/5xx.
* - Backoff exponencial con tope de 30s.
* - Inyectar User-Agent: PolySignal/1.0 en todas las peticiones.
*
* Uso:
* const data = await httpGet(url, { headers, timeout, retries });
* const data = await httpPost(url, body, { headers, timeout, retries });
*/
import { logger } from './logger.js';
const DEFAULT_TIMEOUT_MS = 10_000;
const DEFAULT_RETRIES = 3;
const RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);
function backoff(attempt) {
return Math.min(1_000 * 2 ** attempt, 30_000);
}
async function request(url, init = {}, { timeout = DEFAULT_TIMEOUT_MS, retries = DEFAULT_RETRIES } = {}) {
for (let attempt = 0; attempt <= retries; attempt++) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, {
...init,
headers: { 'User-Agent': 'PolySignal/1.0', ...init.headers },
signal: controller.signal,
});
clearTimeout(timeoutId);
if (RETRYABLE_STATUSES.has(res.status) && attempt < retries) {
const wait = backoff(attempt);
logger.warn({ url, status: res.status, attempt, wait }, 'retrying request');
await new Promise((r) => setTimeout(r, wait));
continue;
}
const text = await res.text();
const data = text ? JSON.parse(text) : null;
if (!res.ok) {
const err = Object.assign(new Error(`HTTP ${res.status} — ${url}`), {
status: res.status,
body: data,
});
throw err;
}
return data;
} catch (err) {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
throw Object.assign(new Error(`Timeout after ${timeout}ms — ${url}`), { code: 'TIMEOUT' });
}
// network error (no HTTP status) → retry
if (!err.status && attempt < retries) {
const wait = backoff(attempt);
logger.warn({ url, err: err.message, attempt, wait }, 'network error, retrying');
await new Promise((r) => setTimeout(r, wait));
continue;
}
logger.error({ url, err: err.message }, 'request failed');
throw err;
}
}
}
export const httpGet = (url, { headers, ...opts } = {}) =>
request(url, { method: 'GET', headers }, opts);
export const httpPost = (url, body, { headers, ...opts } = {}) =>
request(
url,
{ method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify(body) },
opts,
);
|