import { pageFetch } from '$lib/utils/navigationAbort'; import { getApiUrl } from '$lib/api/api-utils'; export class ApiError extends Error { readonly status: number; readonly code: string; readonly details: unknown; constructor(status: number, message: string, code = '', details: unknown = null) { super(message); this.name = 'ApiError'; this.status = status; this.code = code; this.details = details; } } interface RequestOptions extends Omit { signal?: AbortSignal; raw?: boolean; cache?: RequestCache; } async function handleResponse(res: Response): Promise { if (!res.ok) { const text = await res.text().catch(() => ''); let message = text || `Request failed with status ${res.status}`; let code = ''; let details: unknown = null; try { const parsed = JSON.parse(text); if (parsed?.error?.message) { message = parsed.error.message; code = parsed.error.code ?? ''; details = parsed.error.details ?? null; } else if (parsed?.detail) { message = parsed.detail; } } catch { // text wasn't JSON — use raw text as message } throw new ApiError(res.status, message, code, details); } if (res.status === 204 || res.headers.get('content-length') === '0') { return undefined as T; } const text = await res.text().catch(() => ''); if (text.trim() === '') { return undefined as T; } try { return JSON.parse(text) as T; } catch { throw new ApiError(res.status, 'Failed to parse response JSON'); } } type FetchFn = typeof fetch; interface ApiClient { get(url: string, opts?: RequestOptions): Promise; post(url: string, body?: unknown, opts?: RequestOptions): Promise; put(url: string, body?: unknown, opts?: RequestOptions): Promise; patch(url: string, body?: unknown, opts?: RequestOptions): Promise; delete(url: string, opts?: RequestOptions): Promise; head(url: string, opts?: RequestOptions): Promise; upload(url: string, body: FormData, opts?: RequestOptions): Promise; } function createClient(fetchFn: FetchFn): ApiClient { async function request( method: string, url: string, body?: unknown, opts?: RequestOptions ): Promise { const { raw, ...fetchOpts } = opts ?? {}; const init: RequestInit = { method, ...fetchOpts }; if (body !== undefined && body !== null) { if (body instanceof FormData) { init.body = body; } else { const headers = new Headers(init.headers as HeadersInit | undefined); headers.set('Content-Type', 'application/json'); init.headers = headers; init.body = JSON.stringify(body); } } const requestUrl = getApiUrl(url); const res = await fetchFn(requestUrl, init); if (raw) return res as unknown as T; return handleResponse(res); } return { get: (url: string, opts?: RequestOptions) => request('GET', url, undefined, opts), post: (url: string, body?: unknown, opts?: RequestOptions) => request('POST', url, body, opts), put: (url: string, body?: unknown, opts?: RequestOptions) => request('PUT', url, body, opts), patch: (url: string, body?: unknown, opts?: RequestOptions) => request('PATCH', url, body, opts), delete: (url: string, opts?: RequestOptions) => request('DELETE', url, undefined, opts), head: (url: string, opts?: RequestOptions) => request('HEAD', url, undefined, { ...opts, raw: true }), upload: (url: string, body: FormData, opts?: RequestOptions) => request('POST', url, body, opts) }; } const navClient = createClient(pageFetch); const globalClient = createClient((...args) => globalThis.fetch(...args)); export const api = Object.assign(navClient, { global: globalClient });