ghostdrive1's picture
Upload folder using huggingface_hub
0b9dc2e verified
Raw
History Blame Contribute Delete
3.58 kB
import { toast } from 'sonner';
export const getBaseUrl = () => localStorage.getItem('server_url') ?? '';
export const getUserId = () => localStorage.getItem('username') ?? '';
/**
* Structured error thrown for non-2xx HTTP responses.
* `message` contains the human-readable detail extracted from the backend.
*/
export class ApiError extends Error {
readonly status: number;
readonly detail: string;
constructor(status: number, detail: string) {
super(detail);
this.name = 'ApiError';
this.status = status;
this.detail = detail;
}
}
interface RequestOptions {
method?: string;
body?: unknown;
params?: Record<string, string>;
/** When true, suppresses the automatic error toast. Useful when the caller shows its own inline error UI. */
silent?: boolean;
}
function buildHeaders(hasBody: boolean): Record<string, string> {
const headers: Record<string, string> = { 'X-User-ID': getUserId() };
if (hasBody) headers['Content-Type'] = 'application/json';
return headers;
}
/** Parse the response body and extract the `detail` field if the backend returned JSON. */
async function extractErrorDetail(res: Response): Promise<string> {
const text = await res.text();
try {
const json = JSON.parse(text) as { detail?: unknown };
if (typeof json.detail === 'string') return json.detail;
if (json.detail !== undefined) return JSON.stringify(json.detail);
} catch {
// not JSON – fall through
}
return text || res.statusText;
}
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
const { method = 'GET', body, params, silent = false } = options;
const url = new URL(path, getBaseUrl());
if (params) {
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
}
const res = await fetch(url.toString(), {
method,
headers: buildHeaders(body !== undefined),
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
const detail = await extractErrorDetail(res);
const error = new ApiError(res.status, detail);
if (!silent) toast.error(detail);
throw error;
}
if (res.status === 204) return undefined as T;
return res.json() as Promise<T>;
}
async function streamRequest(
path: string,
options: RequestOptions & { signal?: AbortSignal } = {},
): Promise<Response> {
const { method = 'GET', body, params, signal, silent = false } = options;
const url = new URL(path, getBaseUrl());
if (params) {
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
}
const res = await fetch(url.toString(), {
method,
headers: buildHeaders(body !== undefined),
body: body ? JSON.stringify(body) : undefined,
signal,
});
if (!res.ok) {
const detail = await extractErrorDetail(res);
const error = new ApiError(res.status, detail);
if (!silent) toast.error(detail);
throw error;
}
return res;
}
export const client = {
get: <T>(path: string, params?: Record<string, string>) =>
request<T>(path, { method: 'GET', params }),
post: <T>(path: string, body?: unknown, params?: Record<string, string>) =>
request<T>(path, { method: 'POST', body, params }),
patch: <T>(path: string, body?: unknown, params?: Record<string, string>) =>
request<T>(path, { method: 'PATCH', body, params }),
delete: <T = void>(path: string, params?: Record<string, string>) =>
request<T>(path, { method: 'DELETE', params }),
stream: (path: string, options?: RequestOptions & { signal?: AbortSignal }) =>
streamRequest(path, options),
};