2021-09-16
|~4 min read
|641 words
It turns out there are some issues with how Axios handles time outs. In my case, the code was simply never firing, even when the request took longer than my configured timeout.
There are a number of solutions available on a Github thread on the topic.
Because we eschew the use of a global configuration (I previously wrote about the basis of the HTTP pattern here), but instead pass our Axios configuration through on every request, the pattern we adopted takes advantage of Axios interceptors.
As a refresher, the goal of the pattern is an intelligent interface that allows engineers to interact with simple APIs, so far just get
and post
, and get a lot of built in error handling, retries, etc.
It’s a bit of a “batteries included” approach.
Note: For the purposes of simplicity, I’ve removed a lot of the other logic from this example (i.e., header manipulation, retries, etc.). That can be found in my previous post on the HTTP Fetch Patterns. The same can be said for the types of
HttpStatus
andHttpResponse
.
The get
and post
:
export async function get<T = unknown>(
url: string,
options?: Omit<AxiosRequestConfig, "data">,
) {
return request<T>(() => axios({ ...options, url, method: "get" }))
}
export async function post<T = unknown>(
url: string,
options?: AxiosRequestConfig,
) {
return request<T>(() => axios({ ...options, url, method: "post" }))
}
The request
function that these both use:
async function request<T>(
func: () => Promise<AxiosResponse<T>>,
): Promise<HttpResponse<T>> {
try {
const { data } = await func()
return { status: HttpStatus.Ok, data } as const
} catch (error) {
return parseError(error as AxiosError)
}
}
Notice that in the event of an error, we will parse an error using parseError
. That function looks like:
export function parseError(error: AxiosError) {
const { code, response } = error
if (code === "ETIMEDOUT") return { status: HttpStatus.Timeout } as const
if (response && (response.status < 200 || response.status >= 300))
return { status: HttpStatus.BadStatus } as const
return { status: HttpStatus.Unknown } as const
}
This is important because in a moment, when we get into the interceptors, you’ll notice that we’re converting the error that we throw into an “Axios Timeout Error” (i.e., one with an error code of ETIMEDOUT
).
Which brings us to the main point of this article, the Axios interceptors.
export const useTimeout = (config: AxiosRequestConfig): AxiosRequestConfig => {
const cancelToken = axios.CancelToken.source()
const timeoutId = setTimeout(
() => cancelToken.cancel("TIMEOUT"),
config.timeout,
)
return {
...config,
timeoutId,
cancelToken: cancelToken.token,
} as AxiosRequestConfig
}
export const handleTimeout = (error: AxiosError): Promise<any> =>
Promise.reject(
error.message === "TIMEOUT" ? { ...error, code: "ETIMEDOUT" } : error,
)
export const useClearTimeout = (response: AxiosResponse): AxiosResponse => {
clearTimeout((response.config as any)["timeoutId"])
return response
}
// Request interceptors
axios.interceptors.request.use(useTimeout)
// Response interceptors
axios.interceptors.response.use(useClearTimeout, handleTimeout)
A few things that might be worth calling out:
In useTimeout
we’re returning a timeoutId
on the AxiosRequestConfig
- this attribute is not part of the type, which is why we needed to cast it. The reason we need it is so that we can clean up after ourselves when the request is successful (see useClearTimeout
which is the interceptor used on the success track in the response interceptor).
On the other hand, if the response fails (i.e., the promise rejects), we use the handleTimeout
. That is, the cancel
method of the cancelToken
was fired as part of the setTimeout
. When that happens, the request fails and we now are on the failure track in the response. We don’t want to “rescue” this (because remember parseError
assumes the request fails). Instead, we want to make sure that the error that we manually created looks like an Axios timeout error. So, that’s what handleTimeout
does.
And with that, we have a solution to Axios’s flaky (put generously) behavior around timeouts!
Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!