import { ProviderName } from "./types.js";
export type ProviderErrorCode =
| "RATE_LIMIT"
| "AUTH"
| "NOT_FOUND"
| "UPSTREAM"
| "NETWORK"
| "BAD_RESPONSE";
export class ProviderError extends Error {
readonly provider: ProviderName;
readonly code: ProviderErrorCode;
readonly status?: number;
constructor(
provider: ProviderName,
code: ProviderErrorCode,
message: string,
status?: number,
) {
super(message);
this.provider = provider;
this.code = code;
this.status = status;
}
}
interface FetchJsonOptions {
provider: ProviderName;
timeoutMs?: number;
headers?: Record<string, string>;
}
function mapStatusToCode(status: number): ProviderErrorCode {
if (status === 401 || status === 403) {
return "AUTH";
}
if (status === 404) {
return "NOT_FOUND";
}
if (status === 429) {
return "RATE_LIMIT";
}
return "UPSTREAM";
}
export async function fetchJson<T>(
url: string,
options: FetchJsonOptions,
): Promise<T> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 15_000);
try {
const response = await fetch(url, {
headers: options.headers,
signal: controller.signal,
});
const raw = await response.text();
let parsed: unknown = {};
if (raw) {
try {
parsed = JSON.parse(raw);
} catch {
throw new ProviderError(
options.provider,
"BAD_RESPONSE",
"Provider returned non-JSON content.",
response.status,
);
}
}
if (!response.ok) {
throw new ProviderError(
options.provider,
mapStatusToCode(response.status),
`Provider request failed with status ${response.status}.`,
response.status,
);
}
return parsed as T;
} catch (error) {
if (error instanceof ProviderError) {
throw error;
}
throw new ProviderError(
options.provider,
"NETWORK",
`Provider request failed: ${(error as Error).message}`,
);
} finally {
clearTimeout(timeout);
}
}