import type { ApiError } from '../types' import { getToken } from '../auth/session' // Base URL from a Vite env var (never a secret). Default '' = same origin as the app. const BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? '' const TIMEOUT_MS = Number(import.meta.env.VITE_API_TIMEOUT ?? 15000) export class ApiRequestError extends Error { status: number requestId?: string constructor(err: ApiError) { super(err.message) this.name = 'ApiRequestError' this.status = err.status this.requestId = err.requestId } } let lastRequestId: string | null = null /** The X-Request-ID of the most recent response — shown only in a support/debug panel. */ export function getLastRequestId(): string | null { return lastRequestId } export interface RequestOptions { method?: string // eslint-disable-next-line @typescript-eslint/no-explicit-any body?: any headers?: Record signal?: AbortSignal } export async function apiRequest(path: string, opts: RequestOptions = {}): Promise { const controller = new AbortController() const timer = setTimeout(() => controller.abort(), TIMEOUT_MS) const headers: Record = { Accept: 'application/json', ...opts.headers } if (opts.body !== undefined) headers['Content-Type'] = 'application/json' const token = getToken() if (token) headers['Authorization'] = `Bearer ${token}` let res: Response try { res = await fetch(BASE + path, { method: opts.method ?? (opts.body !== undefined ? 'POST' : 'GET'), headers, body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined, credentials: 'include', // carry HTTP-only cookies if the backend uses them signal: opts.signal ?? controller.signal, }) } catch (e) { clearTimeout(timer) const aborted = e instanceof DOMException && e.name === 'AbortError' throw new ApiRequestError({ status: 0, message: aborted ? 'request timed out' : 'network error' }) } clearTimeout(timer) const requestId = res.headers.get('x-request-id') ?? undefined if (requestId) lastRequestId = requestId const text = await res.text() let data: unknown = null if (text) { try { data = JSON.parse(text) } catch { if (!res.ok) throw new ApiRequestError({ status: res.status, message: 'malformed response', requestId }) data = null } } if (!res.ok) { const detail = data && typeof data === 'object' && 'detail' in data ? String((data as { detail: unknown }).detail) : `request failed (${res.status})` throw new ApiRequestError({ status: res.status, message: detail, requestId }) } return data as T }