oneuptime/Common/Utils/API.ts

525 lines
13 KiB
TypeScript
Raw Normal View History

import AnalyticsBaseModel from "../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
import BaseModel from "../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
import HTTPErrorResponse from "../Types/API/HTTPErrorResponse";
import HTTPMethod from "../Types/API/HTTPMethod";
import HTTPResponse from "../Types/API/HTTPResponse";
import Headers from "../Types/API/Headers";
import Hostname from "../Types/API/Hostname";
import Protocol from "../Types/API/Protocol";
import Route from "../Types/API/Route";
import URL from "../Types/API/URL";
import Dictionary from "../Types/Dictionary";
import APIException from "../Types/Exception/ApiException";
import { JSONArray, JSONObject } from "../Types/JSON";
import axios, { AxiosError, AxiosResponse } from "axios";
import Sleep from "../Types/Sleep";
2022-04-08 12:07:15 +00:00
export interface RequestOptions {
retries?: number | undefined;
exponentialBackoff?: boolean | undefined;
}
2022-04-08 12:07:15 +00:00
export default class API {
private _protocol: Protocol = Protocol.HTTPS;
public get protocol(): Protocol {
return this._protocol;
}
public set protocol(v: Protocol) {
this._protocol = v;
}
private _hostname!: Hostname;
public get hostname(): Hostname {
return this._hostname;
}
public set hostname(v: Hostname) {
this._hostname = v;
}
private _baseRoute!: Route;
public get baseRoute(): Route {
return this._baseRoute;
}
public set baseRoute(v: Route) {
this._baseRoute = v;
}
public constructor(
protocol: Protocol,
hostname: Hostname,
baseRoute?: Route,
) {
this.protocol = protocol;
this.hostname = hostname;
if (baseRoute) {
this.baseRoute = baseRoute;
} else {
this.baseRoute = new Route("/");
2022-04-08 12:07:15 +00:00
}
}
public async get<
T extends JSONObject | JSONArray | BaseModel | Array<BaseModel>,
>(
path: Route,
data?: JSONObject | JSONArray,
headers?: Headers,
options?: RequestOptions,
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await API.get<T>(
new URL(this.protocol, this.hostname, this.baseRoute.addRoute(path)),
data,
headers,
options,
);
}
public async delete<
T extends JSONObject | JSONArray | BaseModel | Array<BaseModel>,
>(
path: Route,
data?: JSONObject | JSONArray,
headers?: Headers,
options?: RequestOptions,
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await API.delete<T>(
new URL(this.protocol, this.hostname, this.baseRoute.addRoute(path)),
data,
headers,
options,
);
}
public async head<
T extends JSONObject | JSONArray | BaseModel | Array<BaseModel>,
>(
path: Route,
data?: JSONObject | JSONArray,
headers?: Headers,
options?: RequestOptions,
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await API.head<T>(
new URL(this.protocol, this.hostname, this.baseRoute.addRoute(path)),
data,
headers,
options,
);
}
public async put<
T extends JSONObject | JSONArray | BaseModel | Array<BaseModel>,
>(
path: Route,
data?: JSONObject | JSONArray,
headers?: Headers,
options?: RequestOptions,
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await API.put<T>(
new URL(this.protocol, this.hostname, this.baseRoute.addRoute(path)),
data,
headers,
options,
);
}
public async patch<
T extends JSONObject | JSONArray | BaseModel | Array<BaseModel>,
>(
path: Route,
data?: JSONObject | JSONArray,
headers?: Headers,
options?: RequestOptions,
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await API.patch<T>(
new URL(this.protocol, this.hostname, this.baseRoute.addRoute(path)),
data,
headers,
options,
);
}
public async post<
T extends JSONObject | JSONArray | BaseModel | Array<BaseModel>,
>(
path: Route,
data?: JSONObject | JSONArray,
headers?: Headers,
options?: RequestOptions,
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await API.post<T>(
new URL(this.protocol, this.hostname, this.baseRoute.addRoute(path)),
data,
headers,
options,
);
}
protected static handleError(
error: HTTPErrorResponse | APIException,
): HTTPErrorResponse | APIException {
return error;
}
protected static async onResponseSuccessHeaders(
headers: Dictionary<string>,
): Promise<Dictionary<string>> {
return Promise.resolve(headers);
}
public static getDefaultHeaders(_props?: any): Headers {
const defaultHeaders: Headers = {
"Access-Control-Allow-Origin": "*",
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
};
return defaultHeaders;
}
protected static getHeaders(headers?: Headers): Headers {
let defaultHeaders: Headers = this.getDefaultHeaders();
if (headers) {
defaultHeaders = {
...defaultHeaders,
...headers,
};
2022-04-08 12:07:15 +00:00
}
return defaultHeaders;
}
public static async get<
T extends
| JSONObject
| JSONArray
| BaseModel
| Array<BaseModel>
| AnalyticsBaseModel
| Array<AnalyticsBaseModel>,
>(
url: URL,
data?: JSONObject | JSONArray,
headers?: Headers,
options?: RequestOptions,
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await this.fetch<T>(
HTTPMethod.GET,
url,
data,
headers,
undefined,
options,
);
}
public static async delete<
T extends
| JSONObject
| JSONArray
| BaseModel
| Array<BaseModel>
| AnalyticsBaseModel
| Array<AnalyticsBaseModel>,
>(
url: URL,
data?: JSONObject | JSONArray,
headers?: Headers,
options?: RequestOptions,
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await this.fetch(
HTTPMethod.DELETE,
url,
data,
headers,
undefined,
options,
);
}
public static async head<
T extends
| JSONObject
| JSONArray
| BaseModel
| Array<BaseModel>
| AnalyticsBaseModel
| Array<AnalyticsBaseModel>,
>(
url: URL,
data?: JSONObject | JSONArray,
headers?: Headers,
options?: RequestOptions,
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await this.fetch(
HTTPMethod.HEAD,
url,
data,
headers,
undefined,
options,
);
}
public static async put<
T extends
| JSONObject
| JSONArray
| BaseModel
| Array<BaseModel>
| AnalyticsBaseModel
| Array<AnalyticsBaseModel>,
>(
url: URL,
data?: JSONObject | JSONArray,
headers?: Headers,
options?: RequestOptions,
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await this.fetch(
HTTPMethod.PUT,
url,
data,
headers,
undefined,
options,
);
}
public static async patch<
T extends
| JSONObject
| JSONArray
| BaseModel
| Array<BaseModel>
| AnalyticsBaseModel
| Array<AnalyticsBaseModel>,
>(
url: URL,
data?: JSONObject | JSONArray,
headers?: Headers,
options?: RequestOptions,
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await this.fetch(
HTTPMethod.PATCH,
url,
data,
headers,
undefined,
options,
);
}
public static async post<
T extends
| JSONObject
| JSONArray
| BaseModel
| Array<BaseModel>
| AnalyticsBaseModel
| Array<AnalyticsBaseModel>,
>(
url: URL,
data?: JSONObject | JSONArray,
headers?: Headers,
options?: RequestOptions,
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await this.fetch(
HTTPMethod.POST,
url,
data,
headers,
undefined,
options,
);
}
public static async fetch<
T extends
| JSONObject
| JSONArray
| BaseModel
| Array<BaseModel>
| AnalyticsBaseModel
| Array<AnalyticsBaseModel>,
>(
method: HTTPMethod,
url: URL,
data?: JSONObject | JSONArray,
headers?: Headers,
params?: Dictionary<string>,
options?: RequestOptions,
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
const apiHeaders: Headers = this.getHeaders(headers);
if (params) {
url.addQueryParams(params);
2022-04-08 12:07:15 +00:00
}
try {
const finalHeaders: Dictionary<string> = {
...apiHeaders,
...headers,
};
let finalBody: JSONObject | JSONArray | URLSearchParams | undefined =
data;
// if content-type is form-url-encoded, then stringify the data
if (
finalHeaders["Content-Type"] === "application/x-www-form-urlencoded" &&
data
) {
finalBody = new URLSearchParams(data as Dictionary<string>);
}
let currentRetry: number = 0;
const maxRetries: number = options?.retries || 0;
const exponentialBackoff: boolean = options?.exponentialBackoff || false;
let result: AxiosResponse | null = null;
while (currentRetry <= maxRetries) {
currentRetry++;
try {
result = await axios({
method: method,
url: url.toString(),
headers: finalHeaders,
data: finalBody,
});
break;
} catch (e) {
if (currentRetry <= maxRetries) {
if (exponentialBackoff) {
await Sleep.sleep(2 ** currentRetry * 1000);
}
continue;
} else {
throw e;
}
}
}
if (!result) {
throw new APIException("No response received from server.");
}
result.headers = await this.onResponseSuccessHeaders(
result.headers as Dictionary<string>,
);
const response: HTTPResponse<T> = new HTTPResponse<T>(
result.status,
result.data,
result.headers as Dictionary<string>,
);
return response;
} catch (e) {
const error: Error | AxiosError = e as Error | AxiosError;
let errorResponse: HTTPErrorResponse;
if (axios.isAxiosError(error)) {
// Do whatever you want with native error
errorResponse = this.getErrorResponse(error);
} else {
throw new APIException(error.message);
}
this.handleError(errorResponse);
return errorResponse;
2022-05-23 21:06:12 +00:00
}
}
private static getErrorResponse(error: AxiosError): HTTPErrorResponse {
if (error.response) {
return new HTTPErrorResponse(
error.response.status,
error.response.data as JSONObject | JSONArray,
error.response.headers as Dictionary<string>,
);
2022-05-23 21:06:12 +00:00
}
// get url from error
const url: string = error?.config?.url || "";
2022-04-08 12:07:15 +00:00
throw new APIException(
`Error occurred while making request to ${url}.`,
error,
);
}
2022-04-08 12:07:15 +00:00
public static getFriendlyErrorMessage(error: AxiosError | Error): string {
const errorString: string = error.message || error.toString();
2022-04-08 12:07:15 +00:00
if (errorString.toLocaleLowerCase().includes("network error")) {
return "Network Error.";
2023-07-26 11:48:54 +00:00
}
if (errorString.toLocaleLowerCase().includes("timeout")) {
return "Timeout Error.";
2022-04-08 12:07:15 +00:00
}
if (errorString.toLocaleLowerCase().includes("request aborted")) {
return "Request Aborted.";
2022-04-08 12:07:15 +00:00
}
if (errorString.toLocaleLowerCase().includes("canceled")) {
return "Request Canceled.";
2022-04-08 12:07:15 +00:00
}
if (errorString.toLocaleLowerCase().includes("connection refused")) {
return "Connection Refused.";
2022-07-26 19:47:36 +00:00
}
if (errorString.toLocaleLowerCase().includes("connection reset")) {
return "Connection Reset.";
2022-04-08 12:07:15 +00:00
}
if (errorString.toLocaleLowerCase().includes("connection closed")) {
return "Connection Closed.";
2022-04-08 12:07:15 +00:00
}
if (errorString.toLocaleLowerCase().includes("connection failed")) {
return "Connection Failed.";
2022-04-08 12:07:15 +00:00
}
if (errorString.toLocaleLowerCase().includes("enotfound")) {
return "Cannot Find Host.";
2022-04-08 12:07:15 +00:00
}
if (errorString.toLocaleLowerCase().includes("econnreset")) {
return "Connection Reset.";
2023-07-26 11:48:54 +00:00
}
if (errorString.toLocaleLowerCase().includes("econnrefused")) {
return "Connection Refused.";
2022-04-08 12:07:15 +00:00
}
if (errorString.toLocaleLowerCase().includes("econnaborted")) {
return "Connection Aborted.";
2022-04-08 12:07:15 +00:00
}
if (errorString.toLocaleLowerCase().includes("certificate has expired")) {
return "SSL Certificate Expired.";
2022-04-08 12:07:15 +00:00
}
if (
errorString
.toLocaleLowerCase()
.includes("certificate signed by unknown authority")
) {
return "SSL Certificate Signed By Unknown Authority.";
2022-04-08 12:07:15 +00:00
}
if (errorString.toLocaleLowerCase().includes("self-signed certificate")) {
return "Self Signed Certificate.";
}
return errorString;
}
2022-04-08 12:07:15 +00:00
}