100 lines
2.6 KiB
TypeScript
100 lines
2.6 KiB
TypeScript
import { isApiError, type ApiError } from "@/lib/errors"
|
|
|
|
type JsonRequestInit = Omit<RequestInit, "body" | "headers"> & {
|
|
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<T>(path: string, init?: JsonRequestInit): Promise<T> {
|
|
if (/^https?:\/\//.test(path)) {
|
|
throw new Error("Absolute URLs are not allowed")
|
|
}
|
|
|
|
let url = path
|
|
if (
|
|
typeof window === "undefined" &&
|
|
path.startsWith("/") &&
|
|
process.env.NEXT_PUBLIC_BACKEND_URL
|
|
) {
|
|
url = `${process.env.NEXT_PUBLIC_BACKEND_URL.replace(/\/$/, "")}${path}`
|
|
}
|
|
|
|
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(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
|
|
}
|