import AnalyticsBaseModel from "../Models/BaseModels/AnalyticsBaseModel/BaseModel"; 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"; export interface RequestOptions { retries?: number | undefined; exponentialBackoff?: boolean | undefined; } 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("/"); } } public async get< T extends JSONObject | JSONArray | BaseModel | Array, >( path: Route, data?: JSONObject | JSONArray, headers?: Headers, options?: RequestOptions, ): Promise | HTTPErrorResponse> { return await API.get( new URL(this.protocol, this.hostname, this.baseRoute.addRoute(path)), data, headers, options, ); } public async delete< T extends JSONObject | JSONArray | BaseModel | Array, >( path: Route, data?: JSONObject | JSONArray, headers?: Headers, options?: RequestOptions, ): Promise | HTTPErrorResponse> { return await API.delete( new URL(this.protocol, this.hostname, this.baseRoute.addRoute(path)), data, headers, options, ); } public async head< T extends JSONObject | JSONArray | BaseModel | Array, >( path: Route, data?: JSONObject | JSONArray, headers?: Headers, options?: RequestOptions, ): Promise | HTTPErrorResponse> { return await API.head( new URL(this.protocol, this.hostname, this.baseRoute.addRoute(path)), data, headers, options, ); } public async put< T extends JSONObject | JSONArray | BaseModel | Array, >( path: Route, data?: JSONObject | JSONArray, headers?: Headers, options?: RequestOptions, ): Promise | HTTPErrorResponse> { return await API.put( new URL(this.protocol, this.hostname, this.baseRoute.addRoute(path)), data, headers, options, ); } public async post< T extends JSONObject | JSONArray | BaseModel | Array, >( path: Route, data?: JSONObject | JSONArray, headers?: Headers, options?: RequestOptions, ): Promise | HTTPErrorResponse> { return await API.post( 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, ): Promise> { 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, }; } return defaultHeaders; } public static async get< T extends | JSONObject | JSONArray | BaseModel | Array | AnalyticsBaseModel | Array, >( url: URL, data?: JSONObject | JSONArray, headers?: Headers, options?: RequestOptions, ): Promise | HTTPErrorResponse> { return await this.fetch( HTTPMethod.GET, url, data, headers, undefined, options, ); } public static async delete< T extends | JSONObject | JSONArray | BaseModel | Array | AnalyticsBaseModel | Array, >( url: URL, data?: JSONObject | JSONArray, headers?: Headers, options?: RequestOptions, ): Promise | HTTPErrorResponse> { return await this.fetch( HTTPMethod.DELETE, url, data, headers, undefined, options, ); } public static async head< T extends | JSONObject | JSONArray | BaseModel | Array | AnalyticsBaseModel | Array, >( url: URL, data?: JSONObject | JSONArray, headers?: Headers, options?: RequestOptions, ): Promise | HTTPErrorResponse> { return await this.fetch( HTTPMethod.HEAD, url, data, headers, undefined, options, ); } public static async put< T extends | JSONObject | JSONArray | BaseModel | Array | AnalyticsBaseModel | Array, >( url: URL, data?: JSONObject | JSONArray, headers?: Headers, options?: RequestOptions, ): Promise | HTTPErrorResponse> { return await this.fetch( HTTPMethod.PUT, url, data, headers, undefined, options, ); } public static async post< T extends | JSONObject | JSONArray | BaseModel | Array | AnalyticsBaseModel | Array, >( url: URL, data?: JSONObject | JSONArray, headers?: Headers, options?: RequestOptions, ): Promise | HTTPErrorResponse> { return await this.fetch( HTTPMethod.POST, url, data, headers, undefined, options, ); } public static async fetch< T extends | JSONObject | JSONArray | BaseModel | Array | AnalyticsBaseModel | Array, >( method: HTTPMethod, url: URL, data?: JSONObject | JSONArray, headers?: Headers, params?: Dictionary, options?: RequestOptions, ): Promise | HTTPErrorResponse> { const apiHeaders: Headers = this.getHeaders(headers); if (params) { url.addQueryParams(params); } try { const finalHeaders: Dictionary = { ...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); } 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, ); const response: HTTPResponse = new HTTPResponse( result.status, result.data, result.headers as Dictionary, ); 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; } } 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, ); } // get url from error const url: string = error?.config?.url || ""; throw new APIException( `Error occurred while making request to ${url}.`, error, ); } public static getFriendlyErrorMessage(error: AxiosError | Error): string { const errorString: string = error.message || error.toString(); if (errorString.toLocaleLowerCase().includes("network error")) { return "Network Error."; } if (errorString.toLocaleLowerCase().includes("timeout")) { return "Timeout Error."; } if (errorString.toLocaleLowerCase().includes("request aborted")) { return "Request Aborted."; } if (errorString.toLocaleLowerCase().includes("canceled")) { return "Request Canceled."; } if (errorString.toLocaleLowerCase().includes("connection refused")) { return "Connection Refused."; } if (errorString.toLocaleLowerCase().includes("connection reset")) { return "Connection Reset."; } if (errorString.toLocaleLowerCase().includes("connection closed")) { return "Connection Closed."; } if (errorString.toLocaleLowerCase().includes("connection failed")) { return "Connection Failed."; } if (errorString.toLocaleLowerCase().includes("enotfound")) { return "Cannot Find Host."; } if (errorString.toLocaleLowerCase().includes("econnreset")) { return "Connection Reset."; } if (errorString.toLocaleLowerCase().includes("econnrefused")) { return "Connection Refused."; } if (errorString.toLocaleLowerCase().includes("econnaborted")) { return "Connection Aborted."; } if (errorString.toLocaleLowerCase().includes("certificate has expired")) { return "SSL Certificate Expired."; } if ( errorString .toLocaleLowerCase() .includes("certificate signed by unknown authority") ) { return "SSL Certificate Signed By Unknown Authority."; } if (errorString.toLocaleLowerCase().includes("self-signed certificate")) { return "Self Signed Certificate."; } return errorString; } }