All checks were successful
Deploy production / deploy (push) Successful in 1m6s
40 lines
1.2 KiB
TypeScript
40 lines
1.2 KiB
TypeScript
import { SourceError, sleep } from './types';
|
|
|
|
const UA = 'stanrunge.dev games tracker (personal, self-hosted)';
|
|
|
|
/** GET/POST JSON with a timeout, retrying 429/5xx a few times (honouring Retry-After). */
|
|
export async function fetchJson<T = unknown>(
|
|
url: string,
|
|
init: RequestInit & { retries?: number; allow?: number[] } = {}
|
|
): Promise<{ status: number; body: T }> {
|
|
const { retries = 3, allow = [], ...rest } = init;
|
|
for (let attempt = 0; ; attempt++) {
|
|
const res = await fetch(url, {
|
|
...rest,
|
|
headers: { 'User-Agent': UA, Accept: 'application/json', ...(rest.headers ?? {}) },
|
|
signal: AbortSignal.timeout(30_000)
|
|
});
|
|
if ((res.status === 429 || res.status >= 500) && attempt < retries) {
|
|
const after = Number(res.headers.get('retry-after'));
|
|
await sleep(
|
|
Math.min(90, Number.isFinite(after) && after > 0 ? after : 5 * 2 ** attempt) * 1000
|
|
);
|
|
continue;
|
|
}
|
|
const text = await res.text();
|
|
if (!res.ok && !allow.includes(res.status)) {
|
|
throw new SourceError(
|
|
`${res.status} from ${new URL(url).host}${new URL(url).pathname}: ${text.slice(0, 160)}`,
|
|
res.status
|
|
);
|
|
}
|
|
let body: unknown;
|
|
try {
|
|
body = text ? JSON.parse(text) : null;
|
|
} catch {
|
|
body = text;
|
|
}
|
|
return { status: res.status, body: body as T };
|
|
}
|
|
}
|