import { getOrCreateSessionId } from "@/lib/sessionId"; // Empty default = same-origin relative URLs. // In prod Caddy proxies /api/* to backend; in dev Next.js rewrites do the same. export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ""; /** * Merges a global X-Session-Id header into the request init. * Only runs in browser (SSR guard). User-supplied headers win over the * auto-injected value so explicit overrides remain backward-compatible. */ function withSessionHeader(init?: RequestInit): RequestInit { const base: RequestInit = init ?? {}; if (typeof window === "undefined") return base; const sessionId = getOrCreateSessionId(); if (!sessionId) return base; return { ...base, headers: { "X-Session-Id": sessionId, ...(base.headers ?? {}), }, }; } export async function apiFetch( path: string, init?: RequestInit, ): Promise { const merged = withSessionHeader(init); const response = await fetch(`${API_BASE_URL}${path}`, { ...merged, headers: { "Content-Type": "application/json", ...(merged.headers ?? {}), }, }); if (!response.ok) { throw new Error(`API error ${response.status}: ${await response.text()}`); } return response.json() as Promise; } /** * Status-aware variant of apiFetch — возвращает status + body для случаев * когда логика зависит от HTTP кода (например 202 Accepted для async jobs). * * Throws на 4xx/5xx (используя HTTPError ниже); 2xx коды все ОК. */ export class HTTPError extends Error { status: number; body: unknown; constructor(status: number, body: unknown, msg?: string) { super(msg ?? `API error ${status}`); this.status = status; this.body = body; } } export async function apiFetchWithStatus( path: string, init?: RequestInit, ): Promise<{ status: number; body: T }> { const merged = withSessionHeader(init); const response = await fetch(`${API_BASE_URL}${path}`, { ...merged, headers: { "Content-Type": "application/json", ...(merged.headers ?? {}), }, }); let body: unknown = null; // 204 No Content имеет пустое тело; для всех остальных читаем как text один раз // (fetch body stream consumable только один раз — нельзя сначала .json() потом .text()). if (response.status !== 204) { const rawText = await response.text(); if (rawText) { try { body = JSON.parse(rawText); } catch { // Не JSON (HTML error page от Caddy / 502 / etc) — оборачиваем в detail body = { detail: rawText }; } } } if (!response.ok) { const detail = (body as { detail?: string })?.detail ?? `API error ${response.status}`; throw new HTTPError(response.status, body, detail); } return { status: response.status, body: body as T }; }