import ky, { type KyInstance, type Options } from "ky"; import { env } from "@/lib/env"; import { resolveApiPrefixUrl } from "@/lib/resolve-api-prefix-url"; import { API_PATHS } from "@/api/api-paths"; import { mapHttpStatusToMessage, normalizeApiRequestError } from "@/api/api-error"; import queryClient from "@/lib/query/query-client"; import { stopQueryBroadcast } from "@/lib/query/setup-query-broadcast"; import { clearAuth, getAccessToken } from "@/lib/auth/auth-storage"; function clearSessionAndRedirectToLogin(): void { clearAuth(); stopQueryBroadcast(queryClient); if (typeof window !== "undefined" && window.location.pathname !== "/login") { window.location.replace("/login"); } } function isAuthLoginRequest(request: Request): boolean { return request.url.includes(API_PATHS.authentication.login); } /** Ky's beforeRequest sees request.body as a stream; check options.body for FormData. */ export function shouldStripJsonContentType(body: unknown): boolean { return body instanceof FormData; } export const api: KyInstance = ky.create({ prefix: resolveApiPrefixUrl(env.apiUrl), headers: { Accept: "application/json", "Content-Type": "application/json", }, hooks: { beforeRequest: [ ({ request, options }) => { const token = getAccessToken(); if (token) { request.headers.set("Authorization", token); } // Let the runtime set multipart boundary; default JSON Content-Type breaks FormData. if (shouldStripJsonContentType(options.body)) { request.headers.delete("Content-Type"); } }, ], beforeError: [({ error }) => normalizeApiRequestError(error)], afterResponse: [ ({ request, response }) => { if (response.status !== 401 || isAuthLoginRequest(request)) { return response; } clearSessionAndRedirectToLogin(); return response; }, ], }, }); export async function apiGet(url: string, options?: Options): Promise { return api.get(url, options).json(); } export async function apiPost(url: string, body?: unknown, options?: Options): Promise { return api.post(url, { ...options, json: body }).json(); } export async function apiPostForm(url: string, body: FormData, options?: Options): Promise { return api.post(url, { ...options, body }).json(); } export async function apiPut(url: string, body?: unknown, options?: Options): Promise { return api.put(url, { ...options, json: body }).json(); } export async function apiPatch(url: string, body?: unknown, options?: Options): Promise { return api.patch(url, { ...options, json: body }).json(); } export async function apiDelete(url: string, options?: Options): Promise { return api.delete(url, options).json(); } export async function apiPostNoContent( url: string, body?: unknown, options?: Options, ): Promise { const response = await api.post(url, { ...options, json: body }); if (response.status === 204) { return; } const text = await response.text(); if (!text.trim()) { return; } JSON.parse(text); } export async function apiDeleteNoContent(url: string, options?: Options): Promise { const response = await api.delete(url, options); if (response.status === 204) { return; } const text = await response.text(); if (!text.trim()) { return; } JSON.parse(text); } export { mapHttpStatusToMessage };