diff options
Diffstat (limited to 'packages/web-util/src/utils/request.ts')
-rw-r--r-- | packages/web-util/src/utils/request.ts | 477 |
1 files changed, 477 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..23d3af468 --- /dev/null +++ b/packages/web-util/src/utils/request.ts @@ -0,0 +1,477 @@ +/* + 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"; + +/** + * @deprecated do not use it, it will be removed + */ +export enum ErrorType { + CLIENT, + SERVER, + UNREADABLE, + TIMEOUT, + UNEXPECTED, +} + + + +/** + * + * @param baseUrl URL where the service is located + * @param endpoint endpoint of the service to be called + * @param options auth, method and params + * @deprecated do not use it, it will be removed + * @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 secret-token:${options.token}`; + } else if (options.basicAuth) { + requestHeaders.Authorization = `Basic ${base64encode( + `${options.basicAuth.username}:${options.basicAuth.password}`, + )}`; + } + requestHeaders["Content-Type"] = + !options.contentType || options.contentType === "json" ? "application/json" : "text/plain"; + + if (options.talerAmlOfficerSignature) { + requestHeaders["Taler-AML-Officer-Signature"] = + options.talerAmlOfficerSignature; + } + + const requestMethod = options?.method ?? "GET"; + const requestBody = options?.data; + const requestTimeout = options?.timeout ?? 5 * 1000; + const requestParams = options.params ?? {}; + const requestPreventCache = options.preventCache ?? false; + const requestPreventCors = options.preventCors ?? false; + + const validURL = validateURL(baseUrl, endpoint); + + if (!validURL) { + const error: HttpResponseUnexpectedError = { + info: { + url: `${baseUrl}${endpoint}`, + payload: {}, + hasToken: !!options.token, + status: 0, + options, + }, + type: ErrorType.UNEXPECTED, + exception: undefined, + loading: false, + message: `invalid URL: "${baseUrl}${endpoint}"`, + }; + throw new RequestError(error) + } + + Object.entries(requestParams).forEach(([key, value]) => { + validURL.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 { + const error: HttpResponseUnexpectedError = { + info: { + url: validURL.href, + payload: {}, + hasToken: !!options.token, + status: 0, + options, + }, + type: ErrorType.UNEXPECTED, + exception: undefined, + loading: false, + message: `unsupported request body type: "${typeof requestBody}"`, + }; + throw new RequestError(error) + } + } + + const controller = new AbortController(); + const timeoutId = setTimeout(() => { + controller.abort("HTTP_REQUEST_TIMEOUT"); + }, requestTimeout); + + let response; + try { + response = await fetch(validURL.href, { + headers: requestHeaders, + method: requestMethod, + credentials: "omit", + mode: requestPreventCors ? "no-cors" : "cors", + cache: requestPreventCache ? "no-cache" : "default", + body: payload, + signal: controller.signal, + }); + } catch (ex) { + const info: RequestInfo = { + payload, + url: validURL.href, + hasToken: !!options.token, + status: 0, + options, + }; + + if (ex instanceof Error) { + if (ex.message === "HTTP_REQUEST_TIMEOUT") { + const error: HttpRequestTimeoutError = { + info, + type: ErrorType.TIMEOUT, + message: "request timeout", + }; + throw new RequestError(error); + } + } + + const error: HttpResponseUnexpectedError = { + info, + type: ErrorType.UNEXPECTED, + exception: ex, + loading: false, + message: (ex instanceof Error ? ex.message : ""), + }; + 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, + validURL.href, + payload, + !!options.token, + options, + ); + return result; + } else { + const dataTxt = await response.text(); + const error = buildRequestFailed( + validURL.href, + dataTxt, + response.status, + payload, + options, + ); + throw new RequestError(error); + } +} + +/** + * @deprecated do not use it, it will be removed + */ +export type HttpResponse<T, ErrorDetail> = + | HttpResponseOk<T> + | HttpResponseLoading<T> + | HttpError<ErrorDetail>; + +/** + * @deprecated do not use it, it will be removed + */ +export type HttpResponsePaginated<T, ErrorDetail> = + | HttpResponseOkPaginated<T> + | HttpResponseLoading<T> + | HttpError<ErrorDetail>; + +/** + * @deprecated do not use it, it will be removed + */ +export interface RequestInfo { + url: string; + hasToken: boolean; + payload: any; + status: number; + options: RequestOptions; +} + +interface HttpResponseLoading<T> { + ok?: false; + loading: true; + clientError?: false; + serverError?: false; + + data?: T; +} +/** + * @deprecated do not use it, it will be removed + */ +export interface HttpResponseOk<T> { + ok: true; + loading?: false; + clientError?: false; + serverError?: false; + + data: T; + info?: RequestInfo; +} + +/** + * @deprecated do not use it, it will be removed + */ +export type HttpResponseOkPaginated<T> = HttpResponseOk<T> & WithPagination; + +/** + * @deprecated do not use it, it will be removed + */ +export interface WithPagination { + loadMore: () => void; + loadMorePrev: () => void; + isReachingEnd?: boolean; + isReachingStart?: boolean; +} + +/** + * @deprecated do not use it, it will be removed + */ +export type HttpError<ErrorDetail> = + | HttpRequestTimeoutError + | HttpResponseClientError<ErrorDetail> + | HttpResponseServerError<ErrorDetail> + | HttpResponseUnreadableError + | HttpResponseUnexpectedError; + +/** + * @deprecated do not use it, it will be removed + */ +export interface HttpResponseServerError<ErrorDetail> { + ok?: false; + loading?: false; + type: ErrorType.SERVER; + payload: ErrorDetail; + status: HttpStatusCode; + message: string; + info: RequestInfo; +} +interface HttpRequestTimeoutError { + ok?: false; + loading?: false; + type: ErrorType.TIMEOUT; + + info: RequestInfo; + + message: string; +} +interface HttpResponseClientError<ErrorDetail> { + ok?: false; + loading?: false; + type: ErrorType.CLIENT; + + info: RequestInfo; + status: HttpStatusCode; + payload: ErrorDetail; + message: string; +} + +interface HttpResponseUnexpectedError { + ok?: false; + loading: false; + type: ErrorType.UNEXPECTED; + + info: RequestInfo; + status?: HttpStatusCode; + exception: unknown; + message: string; +} + +interface HttpResponseUnreadableError { + ok?: false; + loading: false; + type: ErrorType.UNREADABLE; + + info: RequestInfo; + status: HttpStatusCode; + exception: unknown; + body: string; + message: string; +} +/** + * @deprecated do not use it, it will be removed + */ +export class RequestError<ErrorDetail> extends Error { + /** + * @deprecated use cause + */ + info: HttpError<ErrorDetail>; + cause: HttpError<ErrorDetail>; + constructor(d: HttpError<ErrorDetail>) { + super(d.message); + this.info = d; + this.cause = d; + } +} + +type Methods = "GET" | "POST" | "PATCH" | "DELETE" | "PUT"; + +/** + * @deprecated do not use it, it will be removed + */ +export interface RequestOptions { + method?: Methods; + token?: string; + basicAuth?: { + username: string; + password: string; + }; + preventCache?: boolean; + preventCors?: boolean; + data?: any; + params?: unknown; + timeout?: number; + contentType?: "text" | "json"; + talerAmlOfficerSignature?: string; +} + +/** + * @deprecated do not use it, it will be removed + */ +async function buildRequestOk<T>( + response: Response, + url: string, + payload: any, + hasToken: boolean, + options: RequestOptions, +): Promise<HttpResponseOk<T>> { + const dataTxt = await response.text(); + const data = dataTxt ? JSON.parse(dataTxt) : undefined; + return { + ok: true, + data, + info: { + payload, + url, + hasToken, + options, + status: response.status, + }, + }; +} + +/** + * @deprecated do not use it, it will be removed + */ +export function buildRequestFailed<ErrorDetail>( + url: string, + dataTxt: string, + status: number, + payload: any, + maybeOptions?: RequestOptions, +): + | HttpResponseClientError<ErrorDetail> + | HttpResponseServerError<ErrorDetail> + | HttpResponseUnreadableError + | HttpResponseUnexpectedError { + const options = maybeOptions ?? {}; + const info: RequestInfo = { + payload, + url, + hasToken: !!options.token, + options, + status: status || 0, + }; + + // const dataTxt = await response.text(); + try { + const data = dataTxt ? JSON.parse(dataTxt) : undefined; + const errorCode = !data || !data.code ? "" : `(code: ${data.code})`; + const errorHint = + !data || !data.hint ? "Not hint." : `${data.hint} ${errorCode}`; + + if (status && status >= 400 && status < 500) { + const message = + data === undefined + ? `Client error (${status}) without data.` + : errorHint; + + const error: HttpResponseClientError<ErrorDetail> = { + type: ErrorType.CLIENT, + status, + info, + message, + payload: data, + }; + return error; + } + if (status && status >= 500 && status < 600) { + const message = + data === undefined + ? `Server error (${status}) without data.` + : errorHint; + const error: HttpResponseServerError<ErrorDetail> = { + type: ErrorType.SERVER, + status, + info, + message, + payload: data, + }; + return error; + } + return { + info, + loading: false, + type: ErrorType.UNEXPECTED, + status, + exception: undefined, + message: `http status code not handled: ${status}`, + }; + } catch (ex) { + const error: HttpResponseUnreadableError = { + info, + loading: false, + status, + type: ErrorType.UNREADABLE, + exception: ex, + body: dataTxt, + message: "Could not parse body as json", + }; + + return error; + } +} + +/** + * @deprecated do not use it, it will be removed + */ +function validateURL(baseUrl: string, endpoint: string): URL | undefined { + try { + return new URL(`${baseUrl}${endpoint}`) + } catch (ex) { + return undefined + } + +}
\ No newline at end of file |