Handlers in Axios Interceptors Called Multiple Times and How to Avoid It

If you are in the web development field, you might have used Axios to interact with a server.

With more than ten million downloads per week, the comprehensive browser coverage, yes, Axios is such a powerful library. It significantly reduces the pain of the stringify method and JSON.parse when dealing with API. There are also so many other benefits of Axios over the built-in fetch method (If you want to learn more about it, see here).

But today, I want to talk about a weird behavior I found when using the library and how to avoid it.

What happened? Answer: It's called multiple times!!

I was developing a web app, where I had to use a different access token per API request. So I wanted to update the information inside the request header before each API call. To do so, I had to use the axios.interceptors.request.use method to update the request configuration.

So here is how I did it at first.

const updateAuthHeader = async (): Promise<void> => {
  // get new access token
  const { status, data } = await requestAccessToken();
  const success = status === 201;
  if (success) {
    const { access_token } = data;
    // update header
    axios.interceptors.request.use((config) => {
      const c = config;
      if (access_token) {
        c.headers.Authorization = `Bearer ${access_token}`;
      }
      return c;
    }, (error) => Promise.reject(error));
  }
};

What is important is this;

axios.interceptors.request.use((config) => {
  const c = config;
  if (access_token) {
    c.headers.Authorization = `Bearer ${access_token}`;
  }
  return c;
}, (error) => Promise.reject(error));

To update the information in the request header, you have to pass axios.interceptors.request.use a handler that updates the request header.

But when I added console.log() inside the handler, I noticed it is fired multiple times before each request, and as I call APIs, the number of times the handler is called increases. So if I make the third API header update, the method fires the handler three times.

This behavior was not ideal in my case because it makes unnecessary function calls.

But why?

Every time the axios.interceptors.request.use method is called, it has to update the handler's context inside it (in the case above, access_token is different depending on the context). But it turns out that as it updates the handler's context, it stores the handler in an array. And with each update, it fires all the handlers inside the array (see the source code for more details).

I haven't found out the intention behind it, but there is a way to avoid this.

The final code

// store index of handler
let axiosInterceptor: number | null = null;

// clear remaining handlers
const clearInterceptor = (interceptor: number | null) => {
  if (interceptor || interceptor === 0) {
    axios.interceptors.request.eject(interceptor);
  }
};

const updateAuthHeader = async (): Promise<void> => {
  // clear remaining handlers
  clearInterceptor(axiosInterceptor);
  // update access token
  const { status, data } = await requestAccessToken();
  const success = status === 201;
  if (success) {
    const { access_token } = data;
    // store index of handler
    axiosInterceptor = axios.interceptors.request.use((config) => {
      const c = config;
      if (access_token) {
        c.headers.Authorization = `Bearer ${access_token}`;
      }
      return c;
    }, (error) => Promise.reject(error));
  }
};

Notice two important parts.

The first one is axiosInterceptor;

// store the index of handler
let axiosInterceptor: number | null = null;

~~~

axiosInterceptor = axios.interceptors.request.use((config) => {
  handler code here
});

The axios.interceptors.request.use method returns a number value each time it is called. The number will be a handler's index in an array the handler will be stored in. axiosInterceptor stores the index of a new handler to remove it from the array later.

The second one is a function to remove previous handler;

// clear remaining handlers
const clearInterceptor = (interceptor: number | null) => {
  if (interceptor || interceptor === 0) {
    axios.interceptors.request.eject(interceptor);
  }
};

Axios provides a method called eject to remove one of the handlers stored in the array. Before we update the header, we call the method to remove the previous handler from the last time we made an API request. To remove the handler, you must pass its index inside the array to the clearInterceptor function. So when you want to remove the previous handler, you call the function passing the axiosInterceptor variable.

Conclusion

This might not be a big deal for many developers, but I was not too fond that Axios makes unnecessary function calls. Also, I found someone who had the same issue, and the code above is fundamentally the same as their solution. Unfortunately, however, there doesn't seem to be enough discussion about it, so I hope this will be helpful for someone who's facing a similar issue.