import { env } from '../config/env' type RequestJsonOptions = Omit & { timeoutMs?: number body?: unknown } export class HttpError extends Error { public readonly status: number public readonly payload: string constructor(message: string, status: number, payload: string) { super(message) this.status = status this.payload = payload } } export function apiUrl(path: string): string { const normalizedPath = path.startsWith('/') ? path : `/${path}` if (!env.apiBaseUrl) { return normalizedPath } return `${env.apiBaseUrl}${normalizedPath}` } export async function requestJson(path: string, options: RequestJsonOptions = {}): Promise { const controller = new AbortController() const timeoutMs = options.timeoutMs ?? 8000 const headers = new Headers(options.headers || {}) const hasBody = options.body !== undefined if (hasBody && !headers.has('Content-Type')) { headers.set('Content-Type', 'application/json') } const timeout = setTimeout(() => { controller.abort() }, timeoutMs) try { const response = await fetch(apiUrl(path), { ...options, headers, body: hasBody ? JSON.stringify(options.body) : undefined, signal: controller.signal, }) const text = await response.text() if (!response.ok) { throw new HttpError(`Request failed: ${response.status}`, response.status, text) } if (!text) { return {} as T } return JSON.parse(text) as T } finally { clearTimeout(timeout) } }