diff options
author | Sebastian <sebasjm@gmail.com> | 2023-02-08 17:36:26 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-02-08 17:36:26 -0300 |
commit | be01d1479cf650fe8eb0c8e567620abfa4544e1e (patch) | |
tree | a1a0e6f45e6e2fdacca9df77aeb7cf1a065b312d /packages/web-util/src/utils/request.ts | |
parent | f7982ed99672709908d378c7abc02300440a4ac2 (diff) | |
download | wallet-core-be01d1479cf650fe8eb0c8e567620abfa4544e1e.tar.xz |
move request api to web-util
Diffstat (limited to 'packages/web-util/src/utils/request.ts')
-rw-r--r-- | packages/web-util/src/utils/request.ts | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/packages/web-util/src/utils/request.ts b/packages/web-util/src/utils/request.ts new file mode 100644 index 000000000..24342bb80 --- /dev/null +++ b/packages/web-util/src/utils/request.ts @@ -0,0 +1,319 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { base64encode } from "./base64.js"; + +/** + * + * @param baseUrl URL where the service is located + * @param endpoint endpoint of the service to be called + * @param options auth, method and params + * @returns + */ +export async function defaultRequestHandler<T>( + baseUrl: string, + endpoint: string, + options: RequestOptions = {}, +): Promise<HttpResponseOk<T>> { + const requestHeaders: Record<string, string> = {}; + if (options.token) { + requestHeaders.Authorization = `Bearer ${options.token}` + } else if (options.basicAuth) { + requestHeaders.Authorization = `Basic ${base64encode(`${options.basicAuth.username}:${options.basicAuth.password}`)}` + } + requestHeaders["Content-Type"] = options.contentType === "json" ? "application/json" : "text/plain" + + const requestMethod = options?.method ?? "GET"; + const requestBody = options?.data; + const requestTimeout = options?.timeout ?? 2 * 1000; + const requestParams = options.params ?? {}; + + const _url = new URL(`${baseUrl}${endpoint}`); + + 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); + + let response; + try { + response = await fetch(_url.href, { + headers: requestHeaders, + method: requestMethod, + credentials: "omit", + mode: "cors", + body: payload, + signal: controller.signal, + }); + } catch (ex) { + const info: RequestInfo = { + payload, + url: _url.href, + hasToken: !!options.token, + status: 0, + }; + const error: HttpResponseUnexpectedError = { + info, + status: 0, + error: ex, + message: "Request timeout", + }; + throw new RequestError(error); + } + + if (timeoutId) { + clearTimeout(timeoutId); + } + const headerMap = new Headers(); + response.headers.forEach((value, key) => { + headerMap.set(key, value); + }); + + if (response.ok) { + const result = await buildRequestOk<T>( + response, + _url.href, + payload, + !!options.token, + ); + return result; + } else { + const error = await buildRequestFailed( + response, + _url.href, + payload, + !!options.token, + ); + throw new RequestError(error); + } +} + +export type HttpResponse<T, ErrorDetail> = + | HttpResponseOk<T> + | HttpResponseLoading<T> + | HttpError<ErrorDetail>; + +export type HttpResponsePaginated<T, ErrorDetail> = + | HttpResponseOkPaginated<T> + | HttpResponseLoading<T> + | HttpError<ErrorDetail>; + +export interface RequestInfo { + url: string; + hasToken: boolean; + payload: any; + status: number; +} + +interface HttpResponseLoading<T> { + ok?: false; + loading: true; + clientError?: false; + serverError?: false; + + data?: T; +} +export interface HttpResponseOk<T> { + ok: true; + loading?: false; + clientError?: false; + serverError?: false; + + data: T; + info?: RequestInfo; +} + +export type HttpResponseOkPaginated<T> = HttpResponseOk<T> & WithPagination; + +export interface WithPagination { + loadMore: () => void; + loadMorePrev: () => void; + isReachingEnd?: boolean; + isReachingStart?: boolean; +} + +export type HttpError<ErrorDetail> = + | HttpResponseClientError<ErrorDetail> + | HttpResponseServerError<ErrorDetail> + | HttpResponseUnexpectedError; + +export interface HttpResponseServerError<ErrorDetail> { + ok?: false; + loading?: false; + clientError?: false; + serverError: true; + + error?: ErrorDetail; + status: HttpStatusCode; + message: string; + info?: RequestInfo; +} +interface HttpResponseClientError<ErrorDetail> { + ok?: false; + loading?: false; + clientError: true; + serverError?: false; + + info?: RequestInfo; + isUnauthorized: boolean; + isNotfound: boolean; + status: HttpStatusCode; + error?: ErrorDetail; + message: string; +} + +interface HttpResponseUnexpectedError { + ok?: false; + loading?: false; + clientError?: false; + serverError?: false; + + info?: RequestInfo; + status?: HttpStatusCode; + error: unknown; + message: string; +} + +export class RequestError<ErrorDetail> extends Error { + info: HttpError<ErrorDetail>; + constructor(d: HttpError<ErrorDetail>) { + super(d.message) + this.info = d + } +} + +type Methods = "GET" | "POST" | "PATCH" | "DELETE" | "PUT"; + +export interface RequestOptions { + method?: Methods; + token?: string; + basicAuth?: { + username: string, + password: string, + } + data?: any; + params?: unknown; + timeout?: number, + contentType?: "text" | "json" +} + +async function buildRequestOk<T>( + response: Response, + url: string, + payload: any, + hasToken: boolean, +): Promise<HttpResponseOk<T>> { + 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<ErrorDetail>( + response: Response, + url: string, + payload: any, + hasToken: boolean, +): Promise< + | HttpResponseClientError<ErrorDetail> + | HttpResponseServerError<ErrorDetail> + | 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<ErrorDetail> = { + 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<ErrorDetail> = { + 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", + }; + + return error; + } +} + +// export function isAxiosError<T>( +// error: AxiosError | any, +// ): error is AxiosError<T> { +// return error && error.isAxiosError; +// } |