axios: handling timeouts

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 and HttpResponse.

The get and post:

fetch.tsx
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:

request
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:

parseError
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.

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!