import { isApiError, type ApiError } from "@/lib/errors" type JsonRequestInit = Omit & { headers?: HeadersInit body?: BodyInit | null json?: unknown } 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 { json, headers: headersInit, body: bodyInit, ...rest } = init ?? {} const headers = new Headers(headersInit) headers.set("Accept", "application/json") 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(path, { ...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 }