From db02313801b934ded9dd6d92a5087fca0ee851a4 Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sat, 28 Feb 2026 12:17:42 +0800 Subject: [PATCH] feat(api): add httpJson helper --- lib/api/http.ts | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 lib/api/http.ts diff --git a/lib/api/http.ts b/lib/api/http.ts new file mode 100644 index 0000000..49615fc --- /dev/null +++ b/lib/api/http.ts @@ -0,0 +1,58 @@ +import { isApiError, type ApiError } from "@/lib/errors" + +type JsonRequestInit = Omit & { + headers?: HeadersInit + body?: BodyInit | null + json?: unknown +} + +async function readJsonBody(res: Response): Promise { + const text = await res.text() + if (!text) return null + + try { + return JSON.parse(text) as unknown + } catch { + return null + } +} + +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 res = await fetch(path, { + ...rest, + headers, + body, + }) + + const data = await readJsonBody(res) + + if (res.ok) { + return data as T + } + + const apiError = isApiError(data) ? data : null + if (res.status === 401 || apiError?.code === 401) { + throw new Error("UNAUTHORIZED") + } + + if (apiError) { + throw apiError + } + + throw { code: res.status, msg: res.statusText || "Request failed" } satisfies ApiError +}