oneuptime/Common/Utils/API.ts
Simon Larsen 5ae3a5b5ee
refactor: Improve error handling in API class
The API class in API.ts has been updated to improve error handling. The getFriendlyErrorMessage method now includes additional error cases and returns more specific error messages for network errors, timeouts, request aborts, cancellations, connection issues, and SSL certificate expiration. This change enhances the user experience by providing clearer and more informative error messages.
2024-06-13 21:50:10 +01:00

420 lines
12 KiB
TypeScript

import AnalyticsBaseModel from '../AnalyticsModels/BaseModel';
import BaseModel from '../Models/BaseModel';
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';
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<BaseModel>
>(
path: Route,
data?: JSONObject | JSONArray,
headers?: Headers
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await API.get<T>(
new URL(
this.protocol,
this.hostname,
this.baseRoute.addRoute(path)
),
data,
headers
);
}
public async delete<
T extends JSONObject | JSONArray | BaseModel | Array<BaseModel>
>(
path: Route,
data?: JSONObject | JSONArray,
headers?: Headers
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await API.delete<T>(
new URL(
this.protocol,
this.hostname,
this.baseRoute.addRoute(path)
),
data,
headers
);
}
public async head<
T extends JSONObject | JSONArray | BaseModel | Array<BaseModel>
>(
path: Route,
data?: JSONObject | JSONArray,
headers?: Headers
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await API.head<T>(
new URL(
this.protocol,
this.hostname,
this.baseRoute.addRoute(path)
),
data,
headers
);
}
public async put<
T extends JSONObject | JSONArray | BaseModel | Array<BaseModel>
>(
path: Route,
data?: JSONObject | JSONArray,
headers?: Headers
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await API.put<T>(
new URL(
this.protocol,
this.hostname,
this.baseRoute.addRoute(path)
),
data,
headers
);
}
public async post<
T extends JSONObject | JSONArray | BaseModel | Array<BaseModel>
>(
path: Route,
data?: JSONObject | JSONArray,
headers?: Headers
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await API.post<T>(
new URL(
this.protocol,
this.hostname,
this.baseRoute.addRoute(path)
),
data,
headers
);
}
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,
};
}
return defaultHeaders;
}
public static async get<
T extends
| JSONObject
| JSONArray
| BaseModel
| Array<BaseModel>
| AnalyticsBaseModel
| Array<AnalyticsBaseModel>
>(
url: URL,
data?: JSONObject | JSONArray,
headers?: Headers
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await this.fetch<T>(HTTPMethod.GET, url, data, headers);
}
public static async delete<
T extends
| JSONObject
| JSONArray
| BaseModel
| Array<BaseModel>
| AnalyticsBaseModel
| Array<AnalyticsBaseModel>
>(
url: URL,
data?: JSONObject | JSONArray,
headers?: Headers
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await this.fetch(HTTPMethod.DELETE, url, data, headers);
}
public static async head<
T extends
| JSONObject
| JSONArray
| BaseModel
| Array<BaseModel>
| AnalyticsBaseModel
| Array<AnalyticsBaseModel>
>(
url: URL,
data?: JSONObject | JSONArray,
headers?: Headers
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await this.fetch(HTTPMethod.HEAD, url, data, headers);
}
public static async put<
T extends
| JSONObject
| JSONArray
| BaseModel
| Array<BaseModel>
| AnalyticsBaseModel
| Array<AnalyticsBaseModel>
>(
url: URL,
data?: JSONObject | JSONArray,
headers?: Headers
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await this.fetch(HTTPMethod.PUT, url, data, headers);
}
public static async post<
T extends
| JSONObject
| JSONArray
| BaseModel
| Array<BaseModel>
| AnalyticsBaseModel
| Array<AnalyticsBaseModel>
>(
url: URL,
data?: JSONObject | JSONArray,
headers?: Headers
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
return await this.fetch(HTTPMethod.POST, url, data, headers);
}
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>
): Promise<HTTPResponse<T> | HTTPErrorResponse> {
const apiHeaders: Headers = this.getHeaders(headers);
if (params) {
url.addQueryParams(params);
}
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>);
}
const result: AxiosResponse = await axios({
method: method,
url: url.toString(),
headers: finalHeaders,
data: finalBody,
});
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;
}
}
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>
);
}
// get url from error
const url: string = error?.config?.url || '';
throw new APIException(`URL ${url ? url + ' ' : ''}is not available.`);
}
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;
}
}