Search code examples
postaxiosvuejs3interceptorcontent-type

Axios interceptor changes the POST request's content type on 401 retry


I cannot figure out why Axios is changing my request's content-type on retry.

I am creating an axios instance as follows (notice global default header):

import axios, { type AxiosInstance } from "axios";
const api: AxiosInstance = axios.create({
  baseURL: "https://localhost:44316/",
});
export default api;

I import this instance in various components within my vue3 app. When my token has expired and I detect a 401, I use the interceptor to refresh my token and retry the call as follows (using a wait pattern to queue multiple requests and prevent requesting multiple refresh tokens):

  axios.interceptors.request.use(
    (config) => {
      const authStore = useAuthStore();
      if (!authStore.loggedIn) {
        authStore.setUserFromStorage();
        if (!authStore.loggedIn) {
          return config;
        }
      }
      if (config?.headers && authStore.user.accessToken) {
        config.headers = {
          Authorization: `Bearer ${authStore.user.accessToken}`,
        };
      }
      return config;
    },
    (error) => {
      return Promise.reject(error);
    }
  );
axios.interceptors.response.use(
  (res) => {
    return res;
  },
  async (err) => {
    if (err.response.status === 401 && !err.config._retry) {
      console.log("new token required");
      err.config._retry = true;
      const authStore = useAuthStore();
      if (!authStore.isRefreshing) {
        authStore.isRefreshing = true;

        return new Promise((resolve, reject) => {
          console.log("refreshing token");
          axios
            .post("auth/refreshToken", {
              token: authStore.user?.refreshToken,
            })
            .then((res) => {
              authStore.setUserInfo(res.data as User);
              console.log("refresh token received", err.config, res.data);
              resolve(axios(err.config));
            })
            .catch(() => {
              console.log("refresh token ERROR");
              authStore.logout();
            })
            .finally(() => {
              authStore.isRefreshing = false;
            });
        });
      } else {
        // not the first request, wait for first request to finish
        return new Promise((resolve, reject) => {
          const intervalId = setInterval(() => {
            console.log("refresh token - waiting");
            if (!authStore.isRefreshing) {
              clearInterval(intervalId);
              console.log("refresh token - waiting resolved", err.config);
              resolve(axios(err.config));
            }
          }, 100);
        });
      }
    }
    return Promise.reject(err);
  }
);

But when axios retries the post request, it changes the content-type:

enter image description here

versus the original request (with content-type application/json)

enter image description here

I've read every post/example I could possible find with no luck, I am relatively new to axios and any guidance/examples/documentation is greatly appreciated, I'm against the wall.

To clarify, I used this pattern because it was the most complete example I was able to put together using many different sources, I would appreciate if someone had a better pattern.


Solution

  • Here's your problem...

    config.headers = {
      Authorization: `Bearer ${authStore.user.accessToken}`,
    };
    

    You're completely overwriting the headers object in your request interceptor, leaving it bereft of everything other than Authorization.

    Because the replayed err.config has already serialised the request body into a string, removing the previously calculated content-type header means the client has to infer a plain string type.

    What you should do instead is directly set the new header value without overwriting the entire object.

    config.headers.Authorization = `Bearer ${authStore.user.accessToken}`;
    

    See this answer for an approach to queuing requests behind an in-progress (re)authentication request that doesn't involve intervals or timeouts.