/*
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 { HttpStatusCode } from "@gnu-taler/taler-util";
import { base64encode } from "./base64.js";
export enum ErrorType {
CLIENT,
SERVER,
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
* @returns
*/
export async function defaultRequestHandler(
baseUrl: string,
endpoint: string,
options: RequestOptions = {},
): Promise> {
const requestHeaders: Record = {};
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 ?? 5 * 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: HttpRequestTimeoutError = {
clientError: true,
isNotfound: false,
isUnauthorized: false,
error: undefined,
info,
type: ErrorType.TIMEOUT,
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(
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 =
| HttpResponseOk
| HttpResponseLoading
| HttpError;
export type HttpResponsePaginated =
| HttpResponseOkPaginated
| HttpResponseLoading
| HttpError;
export interface RequestInfo {
url: string;
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 =
| HttpRequestTimeoutError
| HttpResponseClientError
| HttpResponseServerError
| HttpResponseUnexpectedError;
export interface HttpResponseServerError {
ok?: false;
loading?: false;
/**
* @deprecated use status
*/
clientError?: false;
/**
* @deprecated use status
*/
serverError: true;
type: ErrorType.SERVER;
/**
* @deprecated use payload
*/
error: ErrorDetail;
payload: ErrorDetail;
status: HttpStatusCode;
message: string;
info?: RequestInfo;
}
interface HttpRequestTimeoutError {
ok?: false;
loading?: false;
/**
* @deprecated use type
*/
clientError: true;
/**
* @deprecated use type
*/
serverError?: false;
type: ErrorType.TIMEOUT;
info?: RequestInfo;
error: undefined;
isUnauthorized: false;
isNotfound: false;
message: string;
}
interface HttpResponseClientError {
ok?: false;
loading?: false;
/**
* @deprecated use type
*/
clientError: true;
/**
* @deprecated use type
*/
serverError?: false;
type: ErrorType.CLIENT;
info?: RequestInfo;
/**
* @deprecated use status
*/
isUnauthorized: boolean;
/**
* @deprecated use status
*/
isNotfound: boolean;
status: HttpStatusCode;
/**
* @deprecated use payload
*/
error: ErrorDetail;
payload: ErrorDetail;
message: string;
}
interface HttpResponseUnexpectedError {
ok?: false;
loading?: false;
/**
* @deprecated use type
*/
clientError?: false;
/**
* @deprecated use type
*/
serverError?: false;
type: ErrorType.UNEXPECTED;
info?: RequestInfo;
status?: HttpStatusCode;
/**
* @deprecated use exception
*/
error: unknown;
exception: unknown;
message: string;
}
export class RequestError extends Error {
/**
* @deprecated use cause
*/
info: HttpError;
cause: HttpError;
constructor(d: HttpError) {
super(d.message);
this.info = d;
this.cause = 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(
response: Response,
url: string,
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: string,
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,
type: ErrorType.CLIENT,
status,
info,
message: data?.hint,
error: data, // remove this
payload: data,
};
return error;
}
if (status && status >= 500 && status < 600) {
const error: HttpResponseServerError = {
serverError: true,
type: ErrorType.SERVER,
status,
info,
message: `${data?.hint} (code ${data?.code})`,
error: data, //remove this
payload: data,
};
return error;
}
return {
info,
type: ErrorType.UNEXPECTED,
status,
error: {}, // remove this
exception: undefined,
message: "NOT DEFINED",
};
} catch (ex) {
const error: HttpResponseUnexpectedError = {
info,
status,
type: ErrorType.UNEXPECTED,
error: ex,
exception: ex,
message: "NOT DEFINED",
};
return error;
}
}
// export function isAxiosError(
// error: AxiosError | any,
// ): error is AxiosError {
// return error && error.isAxiosError;
// }