type ClerkSession = { getToken: () => Promise; }; type ClerkGlobal = { session?: ClerkSession | null; }; export class ApiError extends Error { status: number; data: TData | null; constructor(status: number, message: string, data: TData | null) { super(message); this.name = "ApiError"; this.status = status; this.data = data; } } const resolveClerkToken = async (): Promise => { if (typeof window === "undefined") { return null; } const clerk = (window as unknown as { Clerk?: ClerkGlobal }).Clerk; if (!clerk?.session) { return null; } try { return await clerk.session.getToken(); } catch { return null; } }; export const customFetch = async ( url: string, options: RequestInit ): Promise => { const rawBaseUrl = process.env.NEXT_PUBLIC_API_URL; if (!rawBaseUrl) { throw new Error("NEXT_PUBLIC_API_URL is not set."); } const baseUrl = rawBaseUrl.replace(/\/+$/, ""); const headers = new Headers(options.headers); const hasBody = options.body !== undefined && options.body !== null; if (hasBody && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } if (!headers.has("Authorization")) { const token = await resolveClerkToken(); if (token) { headers.set("Authorization", `Bearer ${token}`); } } const response = await fetch(`${baseUrl}${url}`, { ...options, headers, }); if (!response.ok) { const contentType = response.headers.get("content-type") ?? ""; let errorData: unknown = null; const isJson = contentType.includes("application/json") || contentType.includes("+json"); if (isJson) { errorData = (await response.json().catch(() => null)) as unknown; } else { errorData = await response.text().catch(() => ""); } let message = typeof errorData === "string" && errorData ? errorData : "Request failed"; if (errorData && typeof errorData === "object") { const detail = (errorData as { detail?: unknown }).detail; if (typeof detail === "string" && detail) { message = detail; } else if (Array.isArray(detail) && detail.length) { const first = detail[0] as { msg?: unknown }; if (first && typeof first === "object" && typeof first.msg === "string") { message = first.msg; } } } throw new ApiError(response.status, message, errorData); } if (response.status === 204) { return { data: undefined, status: response.status, headers: response.headers, } as T; } const contentType = response.headers.get("content-type") ?? ""; const isJson = contentType.includes("application/json") || contentType.includes("+json"); if (isJson) { const data = (await response.json()) as unknown; return { data, status: response.status, headers: response.headers } as T; } if (contentType.includes("text/event-stream")) { return { data: response, status: response.status, headers: response.headers, } as T; } const text = await response.text().catch(() => ""); return { data: text, status: response.status, headers: response.headers } as T; };