Created
February 26, 2024 23:13
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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