import { isApiError, type ApiError } from "@/lib/errors" type JsonRequestInit = Omit & { headers?: HeadersInit body?: BodyInit | null json?: unknown } async function getServerHeaders() { if (typeof window !== "undefined") return null try { const mod = await import("next/headers") return await mod.headers() } catch { return null } } async function resolveServerUrl(path: string) { if (typeof window !== "undefined" || !path.startsWith("/")) return path const requestHeaders = await getServerHeaders() const host = requestHeaders?.get("x-forwarded-host") ?? requestHeaders?.get("host") if (host) { const proto = requestHeaders?.get("x-forwarded-proto") ?? (host.startsWith("localhost") || host.startsWith("127.0.0.1") ? "http" : "https") return `${proto}://${host}${path}` } const explicitBase = process.env.NEXT_PUBLIC_BACKEND_URL?.replace(/\/$/, "") || process.env.BACKEND_URL?.replace(/\/$/, "") || process.env.INTERNAL_API_ORIGIN?.replace(/\/$/, "") if (explicitBase) return `${explicitBase}${path}` throw new Error(`Cannot resolve server-side API URL for ${path}`) } async function readJsonBody(res: Response): Promise<{ json: unknown | null; text: string }> { const text = await res.text() if (!text) return { json: null, text: "" } try { return { json: JSON.parse(text) as unknown, text } } catch { return { json: null, text } } } function getCookieValue(name: string): string | null { if (typeof document === "undefined") return null if (!document.cookie) return null for (const part of document.cookie.split("; ")) { if (part.startsWith(`${name}=`)) return part.slice(name.length + 1) } return null } function isApiErrorWithMessage(value: unknown): value is { code: number; message: string } { if (typeof value !== "object" || value === null) return false const v = value as { code?: unknown; message?: unknown } return typeof v.code === "number" && typeof v.message === "string" } export async function httpJson(path: string, init?: JsonRequestInit): Promise { if (/^https?:\/\//.test(path)) { throw new Error("Absolute URLs are not allowed") } const url = await resolveServerUrl(path) const { json, headers: headersInit, body: bodyInit, ...rest } = init ?? {} const headers = new Headers(headersInit) headers.set("Accept", "application/json") const requestHeaders = await getServerHeaders() const cookie = requestHeaders?.get("cookie") if (cookie && !headers.has("cookie")) { headers.set("cookie", cookie) } const body = json === undefined ? bodyInit : JSON.stringify(json) if (json !== undefined && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json") } const method = (rest.method ?? "GET").toUpperCase() if ( (method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE") && !headers.has("XSRF-TOKEN") ) { const xsrfToken = getCookieValue("__Host-XSRF-TOKEN") if (xsrfToken) headers.set("XSRF-TOKEN", xsrfToken) } const res = await fetch(url, { ...rest, headers, body, }) const { json: data, text } = await readJsonBody(res) if (res.ok) { return data as T } const apiError: ApiError | null = isApiError(data) ? data : isApiErrorWithMessage(data) ? { code: data.code, msg: data.message } : null if (res.status === 401 || apiError?.code === 401) { throw new Error("UNAUTHORIZED") } if (apiError) { throw apiError } throw { code: res.status, msg: text || res.statusText || "Request failed", } satisfies ApiError }