/*
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";
/**
*
* @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 ?? 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(
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 =
| HttpResponseClientError
| HttpResponseServerError
| HttpResponseUnexpectedError;
export interface HttpResponseServerError {
ok?: false;
loading?: false;
clientError?: false;
serverError: true;
error?: ErrorDetail;
status: HttpStatusCode;
message: string;
info?: RequestInfo;
}
interface HttpResponseClientError {
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 extends Error {
info: HttpError;
constructor(d: HttpError) {
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(
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,
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",
};
return error;
}
}
// export function isAxiosError(
// error: AxiosError | any,
// ): error is AxiosError {
// return error && error.isAxiosError;
// }