| import type { ApiError } from '../types' |
| import { getToken } from '../auth/session' |
|
|
| |
| 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 |
| |
| export function getLastRequestId(): string | null { |
| return lastRequestId |
| } |
|
|
| export interface RequestOptions { |
| method?: string |
| |
| body?: any |
| headers?: Record<string, string> |
| signal?: AbortSignal |
| } |
|
|
| export async function apiRequest<T>(path: string, opts: RequestOptions = {}): Promise<T> { |
| const controller = new AbortController() |
| const timer = setTimeout(() => controller.abort(), TIMEOUT_MS) |
| const headers: Record<string, string> = { 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', |
| 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 |
| } |
|
|