http.ts•2.44 kB
import { request } from 'undici';
import { logger } from './log.js';
export type RequestOptions = {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
headers?: Record<string, string>;
body?: string | Uint8Array;
timeoutMs?: number;
retries?: number;
backoffMs?: number;
};
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export async function requestJson(url: string, opts: RequestOptions = {}) {
const method = opts.method || 'GET';
const headers = {
Accept: 'application/json',
...(opts.body ? { 'Content-Type': 'application/json' } : {}),
...(opts.headers || {}),
} as Record<string, string>;
const timeoutMs = opts.timeoutMs ?? 5000;
const retries = opts.retries ?? 2;
const baseBackoff = opts.backoffMs ?? 200;
let attempt = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
const controller = new AbortController();
const to = setTimeout(() => controller.abort(new Error('Request timeout')), timeoutMs);
try {
logger.debug('http request', {
method,
url: tryRedactUrl(url),
timeoutMs,
attempt: attempt + 1,
});
const res = await request(url, { method, headers, body: opts.body, signal: controller.signal });
clearTimeout(to);
const data = await res.body.json();
const status = res.statusCode;
const hdrs = res.headers;
if (status >= 500 || status === 429) {
if (attempt < retries) {
attempt++;
const wait = baseBackoff * Math.pow(2, attempt - 1);
logger.warn('http transient error, backing off', { status, waitMs: wait });
await sleep(wait);
continue;
}
}
logger.debug('http response', { status });
return { status, headers: hdrs, data };
} catch (err) {
clearTimeout(to);
if (attempt < retries) {
attempt++;
const wait = baseBackoff * Math.pow(2, attempt - 1);
logger.warn('http error, retrying', { attempt, waitMs: wait, error: (err as Error).message });
await sleep(wait);
continue;
}
logger.error('http request failed', { error: (err as Error).message });
throw err;
}
}
}
function tryRedactUrl(u: string) {
try {
const url = new URL(u);
// Drop query to avoid leaking sensitive params
url.search = '';
return url.toString();
} catch {
return 'invalid-url';
}
}