import { backendBaseUrl as BASE, joinBackendPath } from "./runtime-config"; /** * Error thrown for non-2xx responses. `.message` stays " " 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 { 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(path: string, init?: RequestInit): Promise { // 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 { return (await request(path, init)).blob(); } export { BASE };