Skip to content

Instantly share code, notes, and snippets.

@isocroft
Created February 26, 2024 23:13
Show Gist options
  • Save isocroft/7938835fe9f96a5e46dfe63214466b89 to your computer and use it in GitHub Desktop.
Save isocroft/7938835fe9f96a5e46dfe63214466b89 to your computer and use it in GitHub Desktop.
This is a simple Http client on the browser powered by axios library which makes it easy to make async requests
import axios, { AxiosError, AxiosResponse, AxiosRequestConfig, AxiosHeaders } from "axios";
import { API, authRefreshTokenStorageKeyName, authTokenStorageKeyName, userVerifyTokenStorageKeyName } from "@/constants";
import { handleLogout, isCORSViolation } from "./utils";
export type { AxiosError };
export interface OptionsArgs<BodyType, ParamType = any> {
body?: BodyType;
headers?: { [key: string]: any };
params?: ParamType,
isFormData?: boolean;
[optionKey: string]: unknown;
}
export interface ServerResponse<Data> {
data?: Data;
meta?: {
limit: number;
total: number;
page: number;
pages: number;
};
status: boolean;
message: string;
}
const expandOnError = (
errorMessage: string,
errorCode?: string,
errorResponse?: AxiosResponse
) => {
if (errorMessage === "Network Error" && Boolean(!errorResponse)) {
/* @HINT: The message returned below simply means that a CORS error occured OR the name resolution failed */
return errorCode === undefined
? "We are currently updating our systems... Please try again later"
: "Your ISP seems to have some other network-related issues";
}
return "The browser restricted access to the server response! Please contact admin";
};
const getMessageFromError = (error: AxiosError<ServerResponse<{}>>) => {
let message = "";
const isNotCORSViolation = !isCORSViolation(
"request" in error
? (error?.request as XMLHttpRequest)
: new XMLHttpRequest(),
error?.config
);
/* @HINT: These are the varying ranges of error(s) that can occur when making async HTTP requests */
/* @CHECK: https://axios-http.com/docs/handling_errors */
/* @CHECK: https://www.intricatecloud.io/2020/03/how-to-handle-api-errors-in-your-web-app-using-axios/ */
const isServerResponseEmpty =
!(Boolean(error?.response)) &&
isNotCORSViolation &&
(error?.code === "ERR_NETWORK");
const isServerTimedOut =
error?.code === "ECONNABORTED" || error?.code === "ETIMEDOUT";
const isServerUnreachable = error?.code === "ECONNREFUSED";
let isClientOffline = false
if (typeof window !== "undefined") {
isClientOffline = !window.navigator.onLine;
}
const errorMessagesMap = {
empty: "Your connection seems to be very weak! Try changing your internet provider",
unreachable: "Your ISP seems to have connection issues! Please try again later",
timeout: "Our systems request(s) are timing out! Please try again",
indeterminate: "Something went wrong! Please try again",
offline: "Your internet is unstable! Please check and try again"
};
switch (true) {
case isClientOffline:
message = errorMessagesMap["offline"];
break;
case isServerUnreachable:
message = errorMessagesMap["unreachable"];
break;
case isServerTimedOut:
message = errorMessagesMap["timeout"];
break;
case isServerResponseEmpty:
message = errorMessagesMap["empty"];
break;
default:
message =
expandOnError(error.message, error?.code, error?.response) ||
errorMessagesMap["indeterminate"];
}
return message;
};
/* @HINT: TO ADD THE TOKEN BEFORE EACH REQUEST IF AVAILABLE */
axios.interceptors.request.use((config) => {
const requestURL = config.url;
let token = null;
if (typeof requestURL === "string") {
token = requestURL.endsWith("/student/register") || requestURL.endsWith("/student/verify/nin-details")
? localStorage.getItem(userVerifyTokenStorageKeyName)
: localStorage.getItem(authTokenStorageKeyName);
if (!requestURL.endsWith("/student/verify")) {
config.headers['Authorization'] = token ? `Bearer ${token}` : "";
}
}
return config;
}, (error) => {
return Promise.reject(error);
});
/* HINT: Axios interceptors to transform error message for clientFn */
axios.interceptors.response.use(
function (response: AxiosResponse<ServerResponse<{}>>) {
let serverResponse: ServerResponse<{}>;
if (response?.data?.status) {
if (response?.data?.data) {
serverResponse = response?.data;
} else {
serverResponse = { status: true, message: response?.data.message };
}
} else {
serverResponse = { status: false, message: response?.data.message };
}
response.data = serverResponse;
if (!response) {
const emptyResponseData: ServerResponse<{}> = {
status: false,
message: "The browser restricted access to the server response! Please contact admin"
};
return Promise.reject(new AxiosError(
emptyResponseData.message,
"ERR_EMPTY_RESPONSE",
undefined,
undefined,
{
status: 0,
statusText: "unknown",
config: { headers: new AxiosHeaders({}) },
request: undefined,
headers: {},
data: emptyResponseData
}
));
} else {
return response;
}
},
function (error: AxiosError<ServerResponse<{}>>) {
const refreshToken = localStorage.getItem(authRefreshTokenStorageKeyName);
/* @HINT: Normalize using a default response data object to take the shape of "ServerResponse<{}>" type */
const defaultResponseData: ServerResponse<{}> = {
status: false,
message: getMessageFromError(error)
};
let errorResponse = error?.response;
const isRejectedRequest =
errorResponse?.status === 401 || errorResponse?.status === 403;
/* @HINT: If the response data from the API call is undefined, then use the default response data */
const responseData = errorResponse?.data ?? defaultResponseData;
/* @HINT: Override the response data on the axios error if the response data from the API call is undefined */
if (errorResponse) {
if (!errorResponse?.data) {
errorResponse['data'] = responseData;
}
} else {
errorResponse = {
status: error?.status || 0,
statusText: error?.message || error?.cause?.message || "unknown",
config: error?.config || { headers: new AxiosHeaders({}) },
request: error?.request,
headers: {},
data: responseData
}
}
if (
isRejectedRequest ||
responseData.message.toLowerCase().includes("unauthorized")
) {
if (typeof window !== "undefined") {
const pathname = window.location.pathname;
if (pathname.endsWith("/") || pathname.startsWith("/auth")) {
/* @HINT: No logged-in user, so return error response here */
/* @HINT: Return control flow so refresh token API endpoint isn't called */
return Promise.reject(
new AxiosError(
errorResponse?.data ? errorResponse?.data?.message : responseData.message,
error?.code,
error?.config,
error?.request,
errorResponse
)
);
}
/* @HINT: Log user out immediately */
return handleLogout();
}
return axios.post<ServerResponse<{ token: string, refresh_token: string }>>(
`${API}/api/student/refresh-token`,
{
refresh_token: refreshToken,
}
)
.then((response) => {
if (response) {
if (response.data.status && response.data.message !== "unauthorized") {
if (response.data?.data) {
localStorage.setItem(authTokenStorageKeyName, response.data.data.token);
localStorage.setItem(authRefreshTokenStorageKeyName, response.data.data.refresh_token);
if (errorResponse?.config) {
const config = errorResponse?.config;
if (config.headers) {
config.headers["Authorization"] = `Bearer ${response.data.data.token}`;
}
return axios(config).catch(() => {
Promise.reject(handleLogout());
});
}
}
}
}
throw new Error(
"response unavailable"
);
})
.catch(() => {
return handleLogout();
});
}
return Promise.reject(
new AxiosError(
errorResponse?.data ? errorResponse?.data?.message : responseData.message,
error?.code,
error?.config,
error?.request,
errorResponse
)
);
}
);
export async function client<ResponseType extends Record<string, any>, BodyType = {}, ParamType = any>(
endpoint: string,
method: "GET" | "PATCH" | "POST" | "PUT" | "DELETE" | "HEAD",
{ body, headers: customHeaders, params, ...customConfig }: OptionsArgs<BodyType, ParamType> = {}
): Promise<ServerResponse<ResponseType>> {
let headers = customHeaders ? { ...customHeaders } : {};
if (method === "POST" || method === "PUT") {
headers = {
...headers,
"Content-Type": customConfig.isFormData
? "multipart/form-data"
: "application/json"
};
}
const options: AxiosRequestConfig<BodyType> = {
method,
withCredentials: false,
...customConfig,
headers
};
if (body) {
options.data = body;
}
if (params) {
options.params = params;
}
let axiosResponse: AxiosResponse<ServerResponse<ResponseType>> = {
data: {
status: false,
message: "failure"
},
status: 0,
statusText: "<unknown error>",
headers: {},
config: {
headers: new AxiosHeaders({})
}
};
try {
axiosResponse = await axios(
`${endpoint}`,
options
);
if (axiosResponse?.data && axiosResponse?.data.status === false) {
throw new Error("server signalled failure");
}
return axiosResponse?.data;
} catch (error) {
if (error !== undefined) {
return Promise.reject(error);
}
}
return axiosResponse?.data && axiosResponse?.data.status === false
? Promise.reject(new Error("server signalled failure"))
: axiosResponse?.data;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment