Files
ThothII/frontend/src/api/client.ts
T

58 lines
2.3 KiB
TypeScript

import { backendBaseUrl as BASE, joinBackendPath } from "./runtime-config";
/**
* Error thrown for non-2xx responses. `.message` stays "<status> <body>" for
* backward compatibility; `.status` and `.payload` (parsed JSON body, if any)
* let callers branch on a specific failure — e.g. a `code: "dwh_unreachable"`.
*/
export class ApiError extends Error {
constructor(readonly status: number, readonly bodyText: string, readonly payload: unknown) {
super(`${status} ${bodyText}`);
this.name = "ApiError";
}
}
function requestHeaders(init: RequestInit | undefined): Headers {
const headers = new Headers(init?.headers);
// Fetch supplies the multipart boundary for FormData. Declaring JSON here
// would prevent Fastify from parsing an imported workspace bundle.
if (
init?.body != null
&& !(typeof FormData !== "undefined" && init.body instanceof FormData)
&& !headers.has("content-type")
) {
headers.set("content-type", "application/json");
}
return headers;
}
async function request(path: string, init?: RequestInit): Promise<Response> {
const res = await fetch(joinBackendPath(BASE, path), { ...init, headers: requestHeaders(init) });
if (!res.ok) {
const bodyText = await res.text().catch(() => "");
let payload: unknown;
try { payload = bodyText ? JSON.parse(bodyText) : undefined; } catch { payload = undefined; }
throw new ApiError(res.status, bodyText, payload);
}
return res;
}
export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
// Only declare a JSON content-type when we actually send a body. Body-less
// POSTs (resume, close) would otherwise make Fastify reject the empty body
// with FST_ERR_CTP_EMPTY_JSON_BODY (400).
const res = await request(path, init);
if (res.status === 204) return undefined as T;
// Accepted fire-and-forget endpoints may legitimately return 202 with no
// representation. Keep apiFetch useful for both 202 and 204 contracts.
const body = await res.text();
return body ? (JSON.parse(body) as T) : (undefined as T);
}
/** Download registry bundles without attempting to parse their ZIP body as JSON. */
export async function apiFetchBlob(path: string, init?: RequestInit): Promise<Blob> {
return (await request(path, init)).blob();
}
export { BASE };