53 lines
1.4 KiB
TypeScript
53 lines
1.4 KiB
TypeScript
import { authFetchOrThrow, getApiBase } from "./auth";
|
|
|
|
export type ApiTransport = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
|
|
export class ApiClientError extends Error {
|
|
readonly status: number;
|
|
|
|
constructor(status: number, message: string) {
|
|
super(message);
|
|
this.name = "ApiClientError";
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
const errorMessage = (payload: unknown, fallback: string) => {
|
|
if (!payload || typeof payload !== "object") return fallback;
|
|
const record = payload as Record<string, unknown>;
|
|
for (const key of ["detail", "error", "message"]) {
|
|
const value = record[key];
|
|
if (typeof value === "string" && value.trim()) return value.trim();
|
|
}
|
|
return fallback;
|
|
};
|
|
|
|
export const apiUrl = (path: string) => {
|
|
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
return `${getApiBase()}${normalizedPath}`;
|
|
};
|
|
|
|
export async function requestJson<T>(
|
|
path: string,
|
|
init?: RequestInit,
|
|
transport: ApiTransport = authFetchOrThrow,
|
|
): Promise<T> {
|
|
const response = await transport(apiUrl(path), init);
|
|
if (response.status === 204) return undefined as T;
|
|
|
|
const text = await response.text();
|
|
let payload: unknown = null;
|
|
if (text) {
|
|
try {
|
|
payload = JSON.parse(text);
|
|
} catch {
|
|
payload = text;
|
|
}
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new ApiClientError(response.status, errorMessage(payload, text || `Request failed: ${response.status}`));
|
|
}
|
|
return payload as T;
|
|
}
|