import { getToken, clearAuth } from './auth'; const BASE = '/api/v1'; export class ApiError extends Error { status: number; code: string; constructor(status: number, code: string, message: string) { super(message); this.status = status; this.code = code; } } const TIMEOUT_MS = 20_000; async function request(method: string, path: string, body?: unknown): Promise { const headers: Record = {}; const token = getToken(); if (token) { headers['Authorization'] = `Bearer ${token}`; } if (body !== undefined) { headers['Content-Type'] = 'application/json'; } // Abort hung requests so a dead connection surfaces as a friendly error // instead of a spinner that never resolves. const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); let res: Response; try { res = await fetch(`${BASE}${path}`, { method, headers, body: body !== undefined ? JSON.stringify(body) : undefined, signal: controller.signal }); } catch (e) { if (e instanceof DOMException && e.name === 'AbortError') { throw new ApiError(0, 'timeout', 'Zeitüberschreitung – bitte erneut versuchen.'); } throw new ApiError(0, 'network', 'Netzwerkfehler – bitte Verbindung prüfen.'); } finally { clearTimeout(timer); } if (res.status === 204) { return undefined as T; } // A 5xx behind a proxy (or a crash page) can return HTML, not JSON — parsing // it directly would throw an opaque SyntaxError. Read text, parse defensively. const raw = await res.text(); let data: { error?: string; message?: string } | unknown = null; if (raw) { try { data = JSON.parse(raw); } catch { data = null; } } if (!res.ok) { // An expired/invalid token (401) clears the dead session. Banned users are // NOT logged out — they keep read access by design (USER_JOURNEYS §10) and // simply get a 403 "gesperrt" toast on writes. if (res.status === 401) { clearAuth(); } const d = (data ?? {}) as { error?: string; message?: string }; throw new ApiError( res.status, d.error ?? 'unknown', d.message ?? `Serverfehler (${res.status}).` ); } return data as T; } export const api = { get: (path: string) => request('GET', path), post: (path: string, body?: unknown) => request('POST', path, body), patch: (path: string, body?: unknown) => request('PATCH', path, body), delete: (path: string) => request('DELETE', path) };