LeLab / src /lib /apiClient.ts
GitHub CI
Sync from leLab @ 7317f7103e3a9d7f45fe4c0d6e4660a8f9d295e3
fc9bd9f
export type Fetcher = (
url: string,
options?: RequestInit
) => Promise<Response>;
export class ApiError extends Error {
status: number;
detail: string | null;
constructor(message: string, status: number, detail: string | null) {
super(message);
this.name = "ApiError";
this.status = status;
this.detail = detail;
}
}
export interface ApiRequestOptions {
method?: string;
body?: unknown;
signal?: AbortSignal;
/** Human-readable label for the error message, e.g. "Start training". */
action?: string;
}
/**
* Performs a request against the lelab backend and parses the JSON response.
* Throws ApiError with FastAPI's `detail` field on non-2xx, or on JSON parse
* failure. Use this in place of ad-hoc `r.ok` / `r.json()` branching.
*/
export async function apiRequest<T = unknown>(
baseUrl: string,
fetcher: Fetcher,
path: string,
{ method = "GET", body, signal, action }: ApiRequestOptions = {}
): Promise<T> {
const init: RequestInit = { method, signal };
if (body !== undefined) init.body = JSON.stringify(body);
const url = `${baseUrl}${path}`;
const r = await fetcher(url, init);
if (!r.ok) {
let detail: string | null = null;
try {
const errBody = await r.json();
detail = errBody?.detail ?? errBody?.message ?? null;
} catch {
// body wasn't JSON
}
const label = action || `${method} ${path}`;
throw new ApiError(
`${label} failed: ${detail ?? r.status}`,
r.status,
detail
);
}
// 204 No Content
if (r.status === 204) return undefined as T;
return r.json() as Promise<T>;
}