How can I get an axios interceptor to retry the original request?
Asked Answered
E

4

6

I am trying to implement a token refresh into my vue.js application. This is working so far, as it refreshes the token in the store on a 401 response, but all I need to do is get the interceptor to retry the original request again afterwards.

main.js

axios.interceptors.response.use(
    response => {
        return response;
    },
    error => {
        console.log("original request", error.config);
        if (error.response.status === 401 && error.response.statusText === "Unauthorized") {
            store.dispatch("authRefresh")
                .then(res => {
                    //retry original request???
                })
                .catch(err => {
                    //take user to login page
                    this.router.push("/");
                });
        }
    }
);

store.js

authRefresh(context) {
    return new Promise((resolve, reject) => {
        axios.get("auth/refresh", context.getters.getHeaders)
            .then(response => {
                //set new token in state and storage
                context.commit("addNewToken", response.data.data);
                resolve(response);
            })
            .catch(error => {
                reject(error);
            });
    });
},

I can log the error.config in the console and see the original request, but does anyone have any idea what I do from here to retry the original request? and also stop it from looping over and over if it fails.

Or am I doing this completely wrong? Constructive criticism welcome.

Ernaline answered 15/10, 2019 at 13:11 Comment(0)
P
14

You could do something like this:

axios.interceptors.response.use(function (response) {
  return response;
}, function (error) {

  const originalRequest = error.config;

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

    originalRequest._retry = true;

    const refreshToken = window.localStorage.getItem('refreshToken');
    return axios.post('http://localhost:8000/auth/refresh', { refreshToken })
      .then(({data}) => {
        window.localStorage.setItem('token', data.token);
        window.localStorage.setItem('refreshToken', data.refreshToken);
        axios.defaults.headers.common['Authorization'] = 'Bearer ' + data.token;
        originalRequest.headers['Authorization'] = 'Bearer ' + data.token;
        return axios(originalRequest);
      });
  }

  return Promise.reject(error);
});
Profane answered 15/10, 2019 at 13:22 Comment(2)
can this work when there is multiple request run at the same time ?Rhodian
sorry but don't have any idea for multiple requestsProfane
S
1

Implementation proposed by @Patel Pratik is good but only handles one request at a time.

For multiple requests, you can simply use axios-auth-refresh package. As stated in documentation:

The plugin stalls additional requests that have come in while waiting for a new authorization token and resolves them when a new token is available.

https://www.npmjs.com/package/axios-auth-refresh

Shortwave answered 23/1, 2022 at 11:33 Comment(0)
H
0

Building on @Patel Praik's answer to accommodate multiple requests running at the same time without adding a package:

Sorry I don't know Vue, I use React, but hopefully you can translate the logic over.

What I have done is created a state variable that tracks whether the process of refreshing the token is already in progress. If new requests are made from the client while the token is still refreshing, I keep them in a sleep loop until the new tokens have been received (or getting new tokens failed). Once received break the sleep loop for those requests and retry the original request with the updated tokens:

const refreshingTokens = useRef(false) // variable to track if new tokens have already been requested

const sleep = ms => new Promise(r => setTimeout(r, ms));

axios.interceptors.response.use(function (response) {
    return response;
}, async (error) => {
    const originalRequest = error.config;
    if (error.response.status === 401 && !originalRequest._retry) {
        originalRequest._retry = true;

        // if the app is not already requesting a new token, request new token
        // i.e This is the path that the first request that receives status 401 takes
        if (!refreshingTokens.current) {
            refreshingTokens.current = true //update tracking state to say we are fething new tokens
            const refreshToken = localStorage.getItem('refresh_token')
            try {
                const newTokens = await anAxiosInstanceWithoutInterceptor.post(`${process.env.REACT_APP_API_URL}/user/token-refresh/`, {"refresh": refreshToken});
                localStorage.setItem('access_token', newTokens.data.access);
                localStorage.setItem('refresh_token', newTokens.data.refresh);
                axios.defaults.headers['Authorization'] = "JWT " + newTokens.data.access
                originalRequest.headers['Authorization'] = "JWT " + newTokens.data.access
            refreshingTokens.current = false //update tracking state to say new 
                return axios(originalRequest)
            } catch (e) {
                await deleteTokens()
                setLoggedIn(false)
            }
            refreshingTokens.current = false //update tracking state to say new tokens request has finished
        // if the app is already requesting a new token
        // i.e This is the path the remaining requests which were made at the same time as the first take
        } else {
            // while we are still waiting for the token request to finish, sleep for half a second 
            while (refreshingTokens.current === true) {
                console.log('sleeping')
                await sleep(500);
            }
            originalRequest.headers['Authorization'] = "JWT " + 
            localStorage.getItem('access_token');
            return axios(originalRequest)
        }
    }
    return Promise.reject(error);
});

If you don't want to use a while loop, alternatively you could push any multiple request configs to a state variable array and add an event listener for when the new tokens process is finished, then retry all of the stored arrays.

Hermelindahermeneutic answered 25/2, 2022 at 2:59 Comment(0)
Y
0

@Patel Pratik, thank you.

In react native, I've used async storage and had custom http header, server needed COLLECTORACCESSTOKEN, exactly in that format (don't say why =) Yes, I know, that it shoud be secure storage.

    instance.interceptors.response.use(response => response,
    async error => {                         -----it has to be async
        const originalRequest = error.config;
        const status = error.response?.status;
        if (status === 401 && !originalRequest.isRetry) {
            originalRequest.isRetry = true;
            try {
                const token = await AsyncStorage.getItem('@refresh_token')
                const res = await axios.get(`${BASE_URL}/tokens/refresh/${token}`)
                storeAccess_token(res.data.access_token)
                storeRefresh_token(res.data.refresh_token)
                axios.defaults.headers.common['COLLECTORACCESSTOKEN'] = 
                res.data.access_token;
                originalRequest.headers['COLLECTORACCESSTOKEN'] = 
                res.data.access_token;
                return axios(originalRequest);
            } catch (e) {
                console.log('refreshToken request - error', e)
            }

        }
        if (error.response.status === 503) return
        return Promise.reject(error.response.data);
    });
Yawp answered 15/12, 2022 at 21:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.