import { default as axios, AxiosError, AxiosHeaderValue, AxiosInstance, AxiosRequestConfig, } from 'axios' import { AxiosCacheInstance } from 'axios-cache-interceptor' export const DEFAULT_HEADERS = { Accept: 'application/json', 'Content-Type': 'application/json', } export const NONE = null export const CLIENT_ERROR = 'CLIENT_ERROR' export const SERVER_ERROR = 'SERVER_ERROR' export const TIMEOUT_ERROR = 'TIMEOUT_ERROR' export const CONNECTION_ERROR = 'CONNECTION_ERROR' export const NETWORK_ERROR = 'NETWORK_ERROR' export const UNKNOWN_ERROR = 'UNKNOWN_ERROR' export const CANCEL_ERROR = 'CANCEL_ERROR' type StatusCodes = undefined | number type PROBLEM_CODE = | 'CLIENT_ERROR' | 'SERVER_ERROR' | 'TIMEOUT_ERROR' | 'CONNECTION_ERROR' | 'NETWORK_ERROR' | 'UNKNOWN_ERROR' | 'CANCEL_ERROR' /** * Given a HTTP status code, return back the appropriate problem enum. */ export const getProblemFromStatus = ( status: StatusCodes, ): PROBLEM_CODE | null => { if (!status) return UNKNOWN_ERROR if (status >= 200 && status <= 299) return NONE if (status >= 400 && status <= 499) return CLIENT_ERROR if (status >= 500 && status <= 599) return SERVER_ERROR return UNKNOWN_ERROR } const _isCancel = (e: AxiosError) => { if (axios.isCancel(e)) { return true } return false } /** * Determines the problem from an Axios error. */ export const getProblemFromError = (error: AxiosError): PROBLEM_CODE => { if (error.message === 'Network Error') return NETWORK_ERROR if (_isCancel(error)) return CANCEL_ERROR if ('code' in error && typeof error.code === 'string') { if (['ERR_BAD_REQUEST', 'ERR_BAD_RESPONSE'].includes(error.code)) { return getProblemFromStatus(error.response?.status)! } if (['ECONNABORTED'].includes(error.code)) return TIMEOUT_ERROR if (['ENOTFOUND', 'ECONNREFUSED', 'ECONNRESET'].includes(error.code)) return CONNECTION_ERROR } return getProblemFromStatus(error.response?.status)! } export interface ApiErrorResponse { ok: false problem: PROBLEM_CODE originalError: AxiosError data?: U status?: number headers?: Record config?: AxiosRequestConfig duration?: number } export interface ApiOkResponse { ok: true problem: null originalError: null data?: T status?: number headers?: Record config?: AxiosRequestConfig duration?: number } export type ApiResponse = ApiErrorResponse | ApiOkResponse export interface ApisauceConfig extends AxiosRequestConfig { baseURL: string | undefined axiosInstance?: AxiosInstance } export type Monitor = (response: ApiResponse) => void export type RequestTransform = (request: AxiosRequestConfig) => void export type AsyncRequestTransform = ( request: AxiosRequestConfig, ) => Promise | ((request: AxiosRequestConfig) => Promise) export type ResponseTransform = (response: ApiResponse) => void export type AsyncResponseTransform = ( response: ApiResponse, ) => Promise | ((response: ApiResponse) => Promise) export interface ApisauceInstance { axiosInstance: AxiosInstance | AxiosCacheInstance monitors: ((response: ApiResponse) => void)[] addMonitor: (monitor: (response: ApiResponse) => void) => void requestTransforms: RequestTransform[] asyncRequestTransforms: AsyncRequestTransform[] responseTransforms: ResponseTransform[] asyncResponseTransforms: AsyncResponseTransform[] addRequestTransform: (transform: RequestTransform) => void addAsyncRequestTransform: (transform: AsyncRequestTransform) => void addResponseTransform: (transform: ResponseTransform) => void addAsyncResponseTransform: (transform: AsyncResponseTransform) => void headers: Record setHeader: (key: string, value: string) => AxiosInstance setHeaders: (headers: Record) => AxiosInstance deleteHeader: (name: string) => AxiosInstance setBaseURL: (baseUrl: string) => AxiosInstance getBaseURL: () => string any: (config: AxiosRequestConfig) => Promise> get: ( url: string, params?: object, axiosConfig?: AxiosRequestConfig, ) => Promise> delete: ( url: string, params?: object, axiosConfig?: AxiosRequestConfig, ) => Promise> post: ( url: string, data?: object, axiosConfig?: AxiosRequestConfig, ) => Promise> put: ( url: string, data?: object, axiosConfig?: AxiosRequestConfig, ) => Promise> patch: ( url: string, data?: object, axiosConfig?: AxiosRequestConfig, ) => Promise> } /** * Creates an instance of our API using the configuration. */ export const create = (config: ApisauceConfig): ApisauceInstance => { const headers: Record = { ...DEFAULT_HEADERS, ...(config.headers || {}), } const instance = config.axiosInstance || axios.create({ ...config, headers }) const monitors: ((response: ApiResponse) => void)[] = [] const requestTransforms: ((request: AxiosRequestConfig) => void)[] = [] const asyncRequestTransforms: AsyncRequestTransform[] = [] const responseTransforms: ((response: ApiResponse) => void)[] = [] const asyncResponseTransforms: AsyncResponseTransform[] = [] const addMonitor = ( monitor: (response: ApiResponse) => void, ): void => { monitors.push(monitor) } const addRequestTransform = ( transform: (request: AxiosRequestConfig) => void, ): void => { requestTransforms.push(transform) } const addAsyncRequestTransform = (transform: AsyncRequestTransform): void => { asyncRequestTransforms.push(transform) } const addResponseTransform = ( transform: (response: ApiResponse) => void, ): void => { responseTransforms.push(transform) } const addAsyncResponseTransform = ( transform: AsyncResponseTransform, ): void => { asyncResponseTransforms.push(transform) } const setHeader = (name: string, value: string): AxiosInstance => { headers[name] = value return instance } const setHeaders = (newHeaders: Record): AxiosInstance => { Object.assign(headers, newHeaders) return instance } const deleteHeader = (name: string): AxiosInstance => { delete headers[name] return instance } const setBaseURL = (newURL: string): AxiosInstance => { instance.defaults.baseURL = newURL return instance } const getBaseURL = (): string => instance.defaults.baseURL || '' const doRequest = async ( axiosRequestConfig: AxiosRequestConfig, ): Promise> => { axiosRequestConfig.headers = { ...headers, ...axiosRequestConfig.headers } requestTransforms.forEach((transform) => transform(axiosRequestConfig)) for (const transform of asyncRequestTransforms) { await transform(axiosRequestConfig) } try { const startTime = Date.now() const response = await instance.request(axiosRequestConfig) const duration = Date.now() - startTime const apiResponse: ApiResponse = { ok: true, problem: null, originalError: null, data: response.data, status: response.status, headers: response.headers as Record, config: response.config, duration, } responseTransforms.forEach((transform) => transform(apiResponse)) for (const transform of asyncResponseTransforms) { await transform(apiResponse) } return apiResponse } catch (error) { if (error instanceof AxiosError) { const apiResponse: ApiResponse = { ok: false, problem: getProblemFromError(error), originalError: error, status: error.response?.status, headers: error.response?.headers as Record, config: error.config, } responseTransforms.forEach((transform) => transform(apiResponse)) for (const transform of asyncResponseTransforms) { await transform(apiResponse) } return apiResponse } return { ok: false, problem: UNKNOWN_ERROR, originalError: error as AxiosError, } } } return { axiosInstance: instance, monitors, addMonitor, requestTransforms, asyncRequestTransforms, responseTransforms, asyncResponseTransforms, addRequestTransform, addAsyncRequestTransform, addResponseTransform, addAsyncResponseTransform, headers, setHeader, setHeaders, deleteHeader, setBaseURL, getBaseURL, any: doRequest, get: (url, params, config) => doRequest({ ...config, url, params, method: 'get' }), delete: (url, params, config) => doRequest({ ...config, url, params, method: 'delete' }), post: (url, data, config) => doRequest({ ...config, url, data, method: 'post' }), put: (url, data, config) => doRequest({ ...config, url, data, method: 'put' }), patch: (url, data, config) => doRequest({ ...config, url, data, method: 'patch' }), } } export const { isCancel, CancelToken } = axios export default { DEFAULT_HEADERS, NONE, CLIENT_ERROR, SERVER_ERROR, TIMEOUT_ERROR, CONNECTION_ERROR, NETWORK_ERROR, UNKNOWN_ERROR, create, isCancel, CancelToken, }