Files
History-client/api/http.ts
2026-04-19 23:43:31 +07:00

80 lines
2.2 KiB
TypeScript

export class ApiError extends Error {
status: number;
body: string;
errors: unknown[];
constructor(message: string, status: number, body: string, errors: unknown[] = []) {
super(message);
this.name = "ApiError";
this.status = status;
this.body = body;
this.errors = errors;
}
}
type ApiEnvelope<T> = {
status: "success" | "error" | string;
data: T;
message: string;
errors: unknown[];
};
export async function requestJson<T>(input: RequestInfo | URL, init?: RequestInit): Promise<T> {
const res = await fetch(input, init);
const payload = await parseJsonResponse(res);
const envelope = isApiEnvelope<T>(payload) ? payload : null;
if (!res.ok) {
const message = envelope?.message || `Request failed with status ${res.status}`;
const body = envelope ? message : stringifyPayload(payload);
throw new ApiError(message, res.status, body, envelope?.errors || []);
}
if (envelope) {
if (envelope.status === "error") {
throw new ApiError(envelope.message || "Request failed", res.status, JSON.stringify(envelope), envelope.errors);
}
return envelope.data;
}
return payload as T;
}
export function jsonRequestInit(method: string, body: unknown): RequestInit {
return {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
};
}
async function parseJsonResponse(res: Response): Promise<unknown> {
const text = await res.text();
if (!text.length) return null;
try {
return JSON.parse(text);
} catch {
return text;
}
}
function isApiEnvelope<T>(value: unknown): value is ApiEnvelope<T> {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const source = value as Record<string, unknown>;
return (
"status" in source &&
"data" in source &&
"message" in source &&
"errors" in source
);
}
function stringifyPayload(payload: unknown): string {
if (typeof payload === "string") return payload;
try {
return JSON.stringify(payload);
} catch {
return String(payload);
}
}