export class ApiError extends Error { status: number; detail: unknown; constructor(status: number, detail: unknown) { super(typeof detail === "string" ? detail : `HTTP ${status}`); this.status = status; this.detail = detail; } } function redirectToLogin() { if (typeof window !== "undefined" && !window.location.pathname.startsWith("/login")) { // Full navigation on purpose: clears all client state/query cache after the session expired. // eslint-disable-next-line @next/next/no-location-assign-relative-destination window.location.href = `/login?next=${encodeURIComponent(window.location.pathname)}`; } } export async function api(path: string, init?: RequestInit & { json?: unknown }): Promise { const headers: Record = { Accept: "application/json", ...(init?.headers as Record) }; let body = init?.body; if (init?.json !== undefined) { headers["Content-Type"] = "application/json"; body = JSON.stringify(init.json); } const res = await fetch(path, { ...init, body, headers, credentials: "include" }); if (res.status === 401) { redirectToLogin(); throw new ApiError(401, "authentication required"); } const text = await res.text(); let data: unknown = text; try { data = text ? JSON.parse(text) : null; } catch { /* non-JSON */ } if (!res.ok) { const detail = (data as { detail?: unknown })?.detail ?? data; throw new ApiError(res.status, typeof detail === "string" ? detail : JSON.stringify(detail)); } return data as T; } export const qs = (params: Record) => Object.entries(params) .filter(([, v]) => v !== undefined && v !== null && v !== "") .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`) .join("&");