How to stop infinite loop in axios interceptors
Asked Answered
W

4

1

I would like to stop the infinite loop that I am getting with my axios interceptors. When the user logs in, tokens are set in the local storage. To test if token are being handled well, I change the value of the refresh token, and delete the access token, from the local storage. When i do that, the user is suposed to be redirected to the login page, but it ends up in a infinite loop, unauthorized 401 error.

import axios from "axios";
import { logout } from "../stores/actions/authActions";
import store from "../stores";
import userEnv from "userEnv";
import { REFRESH_TOKEN_ENDPOINT } from "../constants/apiUrlConst";
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from "../constants/tokenConst";

const axiosInstance = axios.create({
  baseURL: userEnv.apiUrl,
});

const refreshAccessToken = async () => {
  const refresh_token = localStorage.getItem(REFRESH_TOKEN_KEY);
  if (!refresh_token) {
    store.dispatch(logout());
    localStorage.clear();
    window.location.href = "/";
  } else {
    try {
      const response = await axiosInstance.get(REFRESH_TOKEN_ENDPOINT);
      const access_token = response.data.access_token;
      const new_refresh_token = response.data.refresh_token;
      localStorage.setItem(ACCESS_TOKEN_KEY, access_token);
      localStorage.setItem(REFRESH_TOKEN_KEY, new_refresh_token);
      return access_token;
    } catch (error) {
      return Promise.reject(error);
    }
  }
};

axiosInstance.interceptors.request.use(
  async (config) => {
    const url = config.url.toLowerCase();
    const method = config.method.toLowerCase();

    const token =
      url === REFRESH_TOKEN_ENDPOINT && method === "get"
        ? localStorage.getItem(REFRESH_TOKEN_KEY)
        : localStorage.getItem(ACCESS_TOKEN_KEY);
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }

    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

axiosInstance.interceptors.response.use(
  (response) => {
    return response;
  },
  async (error) => {
    // 401 Unauthorized
    const originalRequest = error.config;
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      try {
        await refreshAccessToken();
        return axiosInstance(originalRequest);
      } catch (error) {
        return Promise.reject(error);
      }
    }
    return Promise.reject(error);
  }
);

export default axiosInstance;

Could anyone help me find whats causing this loop, and how do i stop it?

Wenonawenonah answered 10/10, 2023 at 8:40 Comment(0)
E
2

One possible solution is to add a boolean flag to track if the refresh token has already been refreshed. this will prevent the interceptor from repeatedly retrying the request if it fails.

const alreadyRefreshed = originalRequest._alreadyRefreshed;
if (error.response?.status === 401 && !alreadyRefreshed) {
  originalRequest._alreadyRefreshed = true;

by checking for this flag, the interceptor will only attempt to refresh the token once, preventing the infinite loop.

Entree answered 10/10, 2023 at 8:46 Comment(1)
thank you for taking the time. I tried the above on the response interceptor, but it didnt work. Same loopWenonawenonah
M
2

i just encountered this problem. Hopefully my answer can help you. TLDR: i solve this by using another axios inside the axiosInstance.(Please correct me if im wrong)

const api = axios.create({
  baseURL: '/api', // This matches the proxy path in your Vite config
  withCredentials: true
});

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        
        console.log("interceptors trying refresh token")
        // Use the /api prefix for the refresh token endpoint
        **//Make Sure U Use Another Axios Here NOT THE INSTANCE**
        const response = await axios.get('/api/auth/refresh-token');
        const {accessToken} = response.data
        //Have to add Payload to Login() function
        store.dispatch(login({ role:loginRoleObj.USER, accessToken }));
        return api(originalRequest);
      } 
      
      catch (refreshError) {
        console.log("Token Refresh Failed",refreshError)
        window.location.href = '/user/login';
        return Promise.reject(refreshError);
      }

    }
    // If it's not a 401 error, or if the request has already been retried, reject the error
    return Promise.reject(error);
  }
);

export default API;

After u do this, the axiosInstance won't clash with the refreshToken API Endpoint. Thus, runs smoothly(I guess).

Megasporophyll answered 8/8 at 17:55 Comment(0)
E
1

returning the axiosInstance call inside the try block and removing the return statement on the refreshToken() function.

axiosInstance.interceptors.response.use(
  (response) => {
    return response;
  },
  async (error) => {
    // 401 Unauthorized
    const originalRequest = error.config;
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      try {
        await refreshAccessToken();
        return axiosInstance(originalRequest);
      } catch (error) {
      }
    }
  }
);
Entree answered 10/10, 2023 at 10:20 Comment(8)
Thanks mate. But i am still getting the same result. Could it be in the refresh token function? I read on the internet that i am not supose to use the same axios instance. Is that true?Wenonawenonah
@Wenonawenonah Yes, it is generally not recommended to use the same axios instance when trying to refresh tokens. This is because interceptors are global and attached to the axios instance, so it can cause unexpected behavior if used for multiple requests.Entree
also trying by removing originalRequest._retry = false; await refreshAccessToken();Entree
remove await refreshToken?Wenonawenonah
no, remove the originalRequest._retry line and just return the axiosInstance callEntree
Same result mateWenonawenonah
@Wenonawenonah i updated the code, try with that, and if still same, then its something elseEntree
Must be something else. Thank you anyway thoWenonawenonah
D
0

An infinite loop in Axios interceptors can be stopped by adding the return statement with the cleanup function. I've used this code to the refresh token in React Native. However, I believe you can gather an idea from it.

const [refreshCall, setRefreshCall] = useState(false);

useEffect(() => {
    const interceptor =  axios.interceptors.response.use(
      (response) => {
        return response;
      },
     async (error) => {
        if (error.response.status === 401 && !refreshCall ) {
          try {
            setRefreshCall(true);
            const newAccessToken = await refreshTokenRequest({refreshToken: refreshTokenInfo});
            dispatch(accessToken(newAccessToken.access_token)); // Redux action call
          } catch  (error) {
            console.log(error)
          }
        } else {
          dispatch(accessToken(null));
       //navigate to the login or action code
        }
        return Promise.reject(error);
      }
    );
    return () => {
      axios.interceptors.response.eject(interceptor); // Clean up the interceptor
    };
  }, [refreshCall, refreshTokenInfo, dispatch]);
Darell answered 8/5 at 16:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.