Axios Interceptors retry original request and access original promise Axios Interceptors retry original request and access original promise vue.js vue.js

Axios Interceptors retry original request and access original promise


Update Feb 13, 2019

As many people have been showing an interest in this topic, I've created the axios-auth-refresh package which should help you to achieve behaviour specified here.


The key here is to return the correct Promise object, so you can use .then() for chaining. We can use Vuex's state for that. If the refresh call happens, we can not only set the refreshing state to true, we can also set the refreshing call to the one that's pending. This way using .then() will always be bound onto the right Promise object, and be executed when the Promise is done. Doing it so will ensure you don't need an extra queue for keeping the calls which are waiting for the token's refresh.

function refreshToken(store) {    if (store.state.auth.isRefreshing) {        return store.state.auth.refreshingCall;    }    store.commit('auth/setRefreshingState', true);    const refreshingCall = Axios.get('get token').then(({ data: { token } }) => {        store.commit('auth/setToken', token)        store.commit('auth/setRefreshingState', false);        store.commit('auth/setRefreshingCall', undefined);        return Promise.resolve(true);    });    store.commit('auth/setRefreshingCall', refreshingCall);    return refreshingCall;}

This would always return either already created request as a Promise or create the new one and save it for the other calls. Now your interceptor would look similar to the following one.

Axios.interceptors.response.use(response => response, error => {    const status = error.response ? error.response.status : null    if (status === 401) {        return refreshToken(store).then(_ => {            error.config.headers['Authorization'] = 'Bearer ' + store.state.auth.token;            error.config.baseURL = undefined;            return Axios.request(error.config);        });    }    return Promise.reject(error);});

This will allow you to execute all the pending requests once again. But all at once, without any querying.


If you want the pending requests to be executed in the order they were actually called, you need to pass the callback as a second parameter to the refreshToken() function, like so.

function refreshToken(store, cb) {    if (store.state.auth.isRefreshing) {        const chained = store.state.auth.refreshingCall.then(cb);        store.commit('auth/setRefreshingCall', chained);        return chained;    }    store.commit('auth/setRefreshingState', true);    const refreshingCall = Axios.get('get token').then(({ data: { token } }) => {        store.commit('auth/setToken', token)        store.commit('auth/setRefreshingState', false);        store.commit('auth/setRefreshingCall', undefined);        return Promise.resolve(token);    }).then(cb);    store.commit('auth/setRefreshingCall', refreshingCall);    return refreshingCall;}

And the interceptor:

Axios.interceptors.response.use(response => response, error => {    const status = error.response ? error.response.status : null    if (status === 401) {        return refreshToken(store, _ => {            error.config.headers['Authorization'] = 'Bearer ' + store.state.auth.token;            error.config.baseURL = undefined;            return Axios.request(error.config);        });    }    return Promise.reject(error);});

I haven't tested the second example, but it should work or at least give you an idea.

Working demo of first example - because of the mock requests and demo version of service used for them, it will not work after some time, still, the code is there.

Source: Interceptors - how to prevent intercepted messages to resolve as an error


Why not try something like this ?

Here I use AXIOS interceptors in both directions. For the outgoing direction I set the Authorization header. For the incoming direction - if there is an error, I return a promise (and AXIOS will try to resolve it). The promise checks what the error was - if it was 401 and we see it for the first time (i.e. we are not inside the retry) then I try to refresh the token. Otherwise I throw the original error.In my case refreshToken() uses AWS Cognito but you can use whatever suits you most. Here I have 2 callbacks for refreshToken():

  1. when the token is successfully refreshed, I retry the AXIOS request using an updated config - including the new fresh token and setting a retry flag so that we do not enter an endless cycle if the API repeatedly responds with 401 errors. We need to pass the resolve and reject arguments to AXIOS or otherwise our fresh new promise will be never resolved/rejected.

  2. if the token could not be refreshed for any reason - we reject the promise. We can not simply throw an error because there might be try/catch block around the callback inside AWS Cognito


Vue.prototype.$axios = axios.create(  {    headers:      {        'Content-Type': 'application/json',      },    baseURL: process.env.API_URL  });Vue.prototype.$axios.interceptors.request.use(  config =>  {    events.$emit('show_spin');    let token = getTokenID();    if(token && token.length) config.headers['Authorization'] = token;    return config;  },  error =>  {    events.$emit('hide_spin');    if (error.status === 401) VueRouter.push('/login'); // probably not needed    else throw error;  });Vue.prototype.$axios.interceptors.response.use(  response =>  {    events.$emit('hide_spin');    return response;  },  error =>  {    events.$emit('hide_spin');    return new Promise(function(resolve,reject)    {      if (error.config && error.response && error.response.status === 401 && !error.config.__isRetry)      {        myVue.refreshToken(function()        {          error.config.__isRetry = true;          error.config.headers['Authorization'] = getTokenID();          myVue.$axios(error.config).then(resolve,reject);        },function(flag) // true = invalid session, false = something else        {          if(process.env.NODE_ENV === 'development') console.log('Could not refresh token');          if(getUserID()) myVue.showFailed('Could not refresh the Authorization Token');          reject(flag);        });      }      else throw error;    });  }); 


This could be done with a single interceptor:

let _refreshToken = '';let _authorizing: Promise<void> | null = null;const HEADER_NAME = 'Authorization';axios.interceptors.response.use(undefined, async (error: AxiosError) => {    if(error.response?.status !== 401) {        return Promise.reject(error);    }    // create pending authorization    _authorizing ??= (_refreshToken ? refresh : authorize)()        .finally(() => _authorizing = null)        .catch(error => Promise.reject(error));    const originalRequestConfig = error.config;    delete originalRequestConfig.headers[HEADER_NAME]; // use from defaults    // delay original requests until authorization has been completed    return _authorizing.then(() => axios.request(originalRequestConfig));});

The rest is an application specific code:

  • Login to api
  • Save/load auth data to/from storage
  • Refresh token

Check out the complete example.