import "server-only"; import { notFound } from "next/navigation"; import type { Envelope } from "./types"; export const API_URL = (process.env.API_URL ?? "http://127.0.0.1:8391").replace(/\/$/, ""); export class ApiRequestError extends Error { constructor( public status: number, public code: string, message: string, ) { super(message); } } /** Server-side fetch of an API envelope. 404 → Next `notFound()`; other failures throw (caught by error.tsx). */ export async function apiEnvelope(path: string, init: { revalidate?: number; timeoutMs?: number } = {}): Promise> { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), init.timeoutMs ?? 12_000); try { const res = await fetch(`${API_URL}${path}`, { signal: ctrl.signal, headers: { accept: "application/json" }, ...(init.revalidate === undefined ? { cache: "no-store" } : { next: { revalidate: init.revalidate } }), }); if (res.status === 404) notFound(); if (!res.ok) { let code = "API_ERROR"; let message = `API ${res.status}`; try { const body = (await res.json()) as { error?: { code?: string; message?: string } }; code = body.error?.code ?? code; message = body.error?.message ?? message; } catch { /* ignore */ } throw new ApiRequestError(res.status, code, message); } return (await res.json()) as Envelope; } finally { clearTimeout(timer); } } export async function api(path: string, init?: { revalidate?: number; timeoutMs?: number }): Promise { return (await apiEnvelope(path, init)).data; } /** Best-effort variant for non-critical widgets: returns null instead of throwing. */ export async function apiOptional(path: string, init?: { revalidate?: number; timeoutMs?: number }): Promise { try { return await api(path, init); } catch (err) { if (err && typeof err === "object" && "digest" in err && String((err as { digest: unknown }).digest).startsWith("NEXT_HTTP_ERROR_FALLBACK")) return null; return null; } }