summaryrefslogtreecommitdiff
path: root/packages/web-util/src/utils/request.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/web-util/src/utils/request.ts')
-rw-r--r--packages/web-util/src/utils/request.ts477
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