/* This file is part of GNU Taler (C) 2021-2023 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ // import axios, { AxiosError, AxiosResponse } from "axios"; import { MerchantBackend } from "../declaration.js"; export async function defaultRequestHandler( base: string, path: string, options: RequestOptions = {}, ): Promise> { const requestHeaders = options.token ? { Authorization: `Bearer ${options.token}` } : undefined; const requestMethod = options?.method ?? "GET"; const requestBody = options?.data; const requestTimeout = 2 * 1000; const requestParams = options.params ?? {}; const _url = new URL(`${base}${path}`); Object.entries(requestParams).forEach(([key, value]) => { _url.searchParams.set(key, String(value)); }); let payload: BodyInit | undefined = undefined; if (requestBody != null) { if (typeof requestBody === "string") { payload = requestBody; } else if (requestBody instanceof ArrayBuffer) { payload = requestBody; } else if (ArrayBuffer.isView(requestBody)) { payload = requestBody; } else if (typeof requestBody === "object") { payload = JSON.stringify(requestBody); } else { throw Error("unsupported request body type"); } } const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort("HTTP_REQUEST_TIMEOUT"); }, requestTimeout); const response = await fetch(_url.href, { headers: { ...requestHeaders, "Content-Type": "text/plain", }, method: requestMethod, credentials: "omit", mode: "cors", body: payload, signal: controller.signal, }); if (timeoutId) { clearTimeout(timeoutId); } const headerMap = new Headers(); response.headers.forEach((value, key) => { headerMap.set(key, value); }); if (response.ok) { const result = await buildRequestOk( response, _url, payload, !!options.token, ); return result; } else { const error = await buildRequestFailed( response, _url, payload, !!options.token, ); throw error; } } export type HttpResponse = | HttpResponseOk | HttpResponseLoading | HttpError; export type HttpResponsePaginated = | HttpResponseOkPaginated | HttpResponseLoading | HttpError; export interface RequestInfo { url: URL; hasToken: boolean; payload: any; status: number; } interface HttpResponseLoading { ok?: false; loading: true; clientError?: false; serverError?: false; data?: T; } export interface HttpResponseOk { ok: true; loading?: false; clientError?: false; serverError?: false; data: T; info?: RequestInfo; } export type HttpResponseOkPaginated = HttpResponseOk & WithPagination; export interface WithPagination { loadMore: () => void; loadMorePrev: () => void; isReachingEnd?: boolean; isReachingStart?: boolean; } export type HttpError = | HttpResponseClientError | HttpResponseServerError | HttpResponseUnexpectedError; export interface SwrError { info: unknown; status: number; message: string; } export interface HttpResponseServerError { ok?: false; loading?: false; clientError?: false; serverError: true; error?: MerchantBackend.ErrorDetail; status: number; message: string; info?: RequestInfo; } interface HttpResponseClientError { ok?: false; loading?: false; clientError: true; serverError?: false; info?: RequestInfo; isUnauthorized: boolean; isNotfound: boolean; status: number; error?: MerchantBackend.ErrorDetail; message: string; } interface HttpResponseUnexpectedError { ok?: false; loading?: false; clientError?: false; serverError?: false; info?: RequestInfo; status?: number; error: unknown; message: string; } type Methods = "GET" | "POST" | "PATCH" | "DELETE" | "PUT"; export interface RequestOptions { method?: Methods; token?: string; data?: any; params?: unknown; } async function buildRequestOk( response: Response, url: URL, payload: any, hasToken: boolean, ): Promise> { const dataTxt = await response.text(); const data = dataTxt ? JSON.parse(dataTxt) : undefined return { ok: true, data, info: { payload, url, hasToken, status: response.status, }, }; } async function buildRequestFailed( response: Response, url: URL, payload: any, hasToken: boolean, ): Promise< | HttpResponseClientError | HttpResponseServerError | HttpResponseUnexpectedError > { const status = response?.status; const info: RequestInfo = { payload, url, hasToken, status: status || 0, }; try { const dataTxt = await response.text(); const data = dataTxt ? JSON.parse(dataTxt) : undefined if (status && status >= 400 && status < 500) { const error: HttpResponseClientError = { clientError: true, isNotfound: status === 404, isUnauthorized: status === 401, status, info, message: data?.hint, error: data, }; return error; } if (status && status >= 500 && status < 600) { const error: HttpResponseServerError = { serverError: true, status, info, message: `${data?.hint} (code ${data?.code})`, error: data, }; return error; } return { info, status, error: {}, message: "NOT DEFINED", }; } catch (ex) { const error: HttpResponseUnexpectedError = { info, status, error: ex, message: "NOT DEFINED", }; throw error; } } // export function isAxiosError( // error: AxiosError | any, // ): error is AxiosError { // return error && error.isAxiosError; // }