interface RequestParams { uri: string; method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT"; allowFail?: boolean; jsonData?: any; formData?: FormData; upProgress?: (progress: number) => void; downProgress?: (e: { progress: number; total: number }) => void; } interface APIResponse { data: any; status: number; } export class ApiError extends Error { constructor(message: string, public code: number, public data: any) { super(`HTTP status: ${code}\nMessage: ${message}\nData=${data}`); } } export class APIClient { /** * Get backend URL */ static backendURL(): string { const URL = import.meta.env.VITE_APP_BACKEND ?? ""; if (URL.length === 0) throw new Error("Backend URL undefined!"); return URL; } /** * Check out whether the backend is accessed through * HTTPS or not */ static IsBackendSecure(): boolean { return this.backendURL().startsWith("https"); } /** * Perform a request on the backend */ static async exec(args: RequestParams): Promise { let body: string | undefined | FormData = undefined; let headers: any = {}; // JSON request if (args.jsonData) { headers["Content-Type"] = "application/json"; body = JSON.stringify(args.jsonData); } // Form data request else if (args.formData) { body = args.formData; } const url = this.backendURL() + args.uri; let data; let status: number; // Make the request with XMLHttpRequest if (args.upProgress) { const res: XMLHttpRequest = await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.upload.addEventListener("progress", (e) => args.upProgress!(e.loaded / e.total) ); xhr.addEventListener("load", () => resolve(xhr)); xhr.addEventListener("error", () => reject(new Error("File upload failed")) ); xhr.addEventListener("abort", () => reject(new Error("File upload aborted")) ); xhr.addEventListener("timeout", () => reject(new Error("File upload timeout")) ); xhr.open(args.method, url, true); xhr.withCredentials = true; for (const key in headers) { if (headers.hasOwnProperty(key)) xhr.setRequestHeader(key, headers[key]); } xhr.send(body); }); status = res.status; if (res.responseType === "json") data = JSON.parse(res.responseText); else data = res.response; } // Make the request with fetch else { const res = await fetch(url, { method: args.method, body: body, headers: headers, credentials: "include", }); // Process response // JSON response if (res.headers.get("content-type") === "application/json") data = await res.json(); // Text / XML response else if ( ["application/xml", "text/plain"].includes( res.headers.get("content-type") ?? "" ) ) data = await res.text(); // Binary file, tracking download progress else if (res.body !== null && args.downProgress) { // Track download progress const contentEncoding = res.headers.get("content-encoding"); const contentLength = contentEncoding ? null : res.headers.get("content-length"); const total = parseInt(contentLength ?? "0", 10); let loaded = 0; const resInt = new Response( new ReadableStream({ start(controller) { const reader = res.body!.getReader(); const read = async () => { try { const ret = await reader.read(); if (ret.done) { controller.close(); return; } loaded += ret.value.byteLength; args.downProgress!({ progress: loaded, total }); controller.enqueue(ret.value); read(); } catch (e) { console.error(e); controller.error(e); } }; read(); }, }) ); data = await resInt.blob(); } // Do not track progress (binary file) else data = await res.blob(); status = res.status; } // Handle expired tokens if (status === 412) { window.location.href = "/"; } if (!args.allowFail && (status < 200 || status > 299)) throw new ApiError("Request failed!", status, data); return { data: data, status: status, }; } }