Repository Public
1import { ProviderError, type HttpClient } from './types.ts';23/** Keep-alive JSON client (Node's fetch pools connections per origin). Refuses redirects and oversized or non-JSON answers. */4export function httpClient(provider: string, fetchImpl: typeof fetch = fetch, maxBytes = 2_000_000): HttpClient {5 const run = async (url: string, init: RequestInit): Promise<unknown> => {6 let r: Response;7 try { r = await fetchImpl(url, { ...init, redirect: 'error' }); }8 catch (e) { throw new ProviderError(provider, (e as Error).name === 'AbortError' || (e as Error).name === 'TimeoutError' ? 'timeout' : 'unavailable', (e as Error).message?.slice(0, 160) ?? 'network error'); }9 if (r.status === 429) throw new ProviderError(provider, 'rate_limited', 'provider rate limit (429)');10 const text = await r.text();11 if (text.length > maxBytes) throw new ProviderError(provider, 'bad_response', 'response too large');12 let body: unknown;13 try { body = text ? JSON.parse(text) : null; } catch { throw new ProviderError(provider, 'bad_response', `non-JSON answer (HTTP ${r.status})`); }14 if (!r.ok) throw Object.assign(new ProviderError(provider, 'http', `HTTP ${r.status}: ${text.slice(0, 160)}`), { status: r.status, body });15 return body;16 };17 return {18 getJson: (url, init) => run(url, { method: 'GET', headers: { accept: 'application/json', ...init?.headers }, signal: init?.signal }),19 postJson: (url, body, init) => run(url, { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json', ...init?.headers }, body: JSON.stringify(body, (_k, v) => (typeof v === 'bigint' ? v.toString() : v)), signal: init?.signal }),20 };21}22