96

I have an interceptor in place to catch 401 errors if the access token expires. If it expires it tries the refresh token to get a new access token. If any other calls are made during this time they are queued until the access token is validated.

This is all working very well. However when processing the queue using Axios(originalRequest) the originally attached promises are not being called. See below for an example.

Working interceptor code:

Axios.interceptors.response.use(
  response => response,
  (error) => {
    const status = error.response ? error.response.status : null
    const originalRequest = error.config

    if (status === 401) {
      if (!store.state.auth.isRefreshing) {
        store.dispatch('auth/refresh')
      }

      const retryOrigReq = store.dispatch('auth/subscribe', token => {
        originalRequest.headers['Authorization'] = 'Bearer ' + token
        Axios(originalRequest)
      })

      return retryOrigReq
    } else {
      return Promise.reject(error)
    }
  }
)

Refresh Method (Used the refresh token to get a new access token)

refresh ({ commit }) {
  commit(types.REFRESHING, true)
  Vue.$http.post('/login/refresh', {
    refresh_token: store.getters['auth/refreshToken']
  }).then(response => {
    if (response.status === 401) {
      store.dispatch('auth/reset')
      store.dispatch('app/error', 'You have been logged out.')
    } else {
      commit(types.AUTH, {
        access_token: response.data.access_token,
        refresh_token: response.data.refresh_token
      })
      store.dispatch('auth/refreshed', response.data.access_token)
    }
  }).catch(() => {
    store.dispatch('auth/reset')
    store.dispatch('app/error', 'You have been logged out.')
  })
},

Subscribe method in auth/actions module:

subscribe ({ commit }, request) {
  commit(types.SUBSCRIBEREFRESH, request)
  return request
},

As well as the Mutation:

[SUBSCRIBEREFRESH] (state, request) {
  state.refreshSubscribers.push(request)
},

Here is a sample action:

Vue.$http.get('/users/' + rootState.auth.user.id + '/tasks').then(response => {
  if (response && response.data) {
    commit(types.NOTIFICATIONS, response.data || [])
  }
})

If this request was added to the queue I because the refresh token had to access a new token I would like to attach the original then():

  const retryOrigReq = store.dispatch('auth/subscribe', token => {
    originalRequest.headers['Authorization'] = 'Bearer ' + token
    // I would like to attache the original .then() as it contained critical functions to be called after the request was completed. Usually mutating a store etc...
    Axios(originalRequest).then(//if then present attache here)
  })

Once the access token has been refreshed the queue of requests is processed:

refreshed ({ commit }, token) {
  commit(types.REFRESHING, false)
  store.state.auth.refreshSubscribers.map(cb => cb(token))
  commit(types.CLEARSUBSCRIBERS)
},
Tim Wickstrom
  • 5,476
  • 3
  • 25
  • 33
  • 3
    You can't get the "original .then() callbacks" and attach them to your new request. Instead, you will need to return a promise for the new result from the interceptor so that it will *resolve* the original promise with the new result. – Bergi Jul 30 '18 at 16:34
  • 2
    I don't know axios or vue in detail, but would assume that something like `const retryOrigReq = store.dispatch('auth/subscribe').then(token => { originalRequest.headers['Authorization'] = 'Bearer ' + token; return Axios(originalRequest) });` should do it – Bergi Jul 30 '18 at 16:36
  • 1
    I updated the question to add additional context. I need to find a way to run the then statements from the original request. In the example it updates the notification store, as an example. – Tim Wickstrom Jul 30 '18 at 17:28
  • Would be nice to know what your `subscribe` action looks like, might help a little. – Dawid Zbiński Jul 30 '18 at 17:48
  • @TimWickstrom Yes, and the only way to run those `then` callbacks is to resolve the promise that the `get(…)` call returned. Afaics, the return value of the interceptor callback provides that ability. – Bergi Jul 30 '18 at 18:09
  • @DawidZbiński great call see updated Questions for subscribe function – Tim Wickstrom Jul 30 '18 at 20:01

3 Answers3

158

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

Dawid Zbiński
  • 5,521
  • 8
  • 43
  • 70
  • It returns the request that was sent to it. – Tim Wickstrom Jul 30 '18 at 20:04
  • I added a working example. You can take a look at it. – Dawid Zbiński Jul 30 '18 at 20:53
  • Thanks, I will plug in the updated code and give it a try. – Tim Wickstrom Jul 30 '18 at 21:02
  • Sure. Should be working, if I explained it good enough. This way you don't need to have any additional queries etc. in you state. The requests waiting will fire up after the interception is finished. That's the main difference. – Dawid Zbiński Jul 30 '18 at 21:05
  • Good, reading through the code that was my first question. There could be MANY requests attempted while the access_token is being refreshed. – Tim Wickstrom Jul 30 '18 at 21:08
  • Sure, and the answer is: it's still working properly, but without you having to create some sort of query. If there's any call on the api while the token is refreshing, it's going to return the same Promise object for each of the request. The Promise object is the object of the "access token obtaining" which, after it's done, will fire all the requests that subscribed to it while it was loading. The working example is updated... you can take a look at AppComponent `tryOut` method. – Dawid Zbiński Jul 30 '18 at 21:12
  • Thanks for the quick response. I will plug this into my app and report back my findings – Tim Wickstrom Jul 30 '18 at 21:15
  • Dawid, you are the man. Works like a champ with some minor updates. – Tim Wickstrom Jul 30 '18 at 21:54
  • Great to hear that. If you find some time, please, edit my answer with things you've changed, so it would help someone else who's also struggling with this. Thanks. – Dawid Zbiński Jul 31 '18 at 04:23
  • Of course! I had to wait 24 hours before I could award it. – Tim Wickstrom Aug 01 '18 at 05:07
  • 1
    One thing I had to add in my Axios interceptor was a check for 401 being returned from the oath/refresh method. If it returned a 401, it would be stuck in a loop. My temporary implementation of this was simply checking the path in an if statement: ```if(location.substring(location.length - 13, location.length) === 'login/refresh') { .. do something }``` – Tim Wickstrom Aug 01 '18 at 20:54
  • Last thought, Axios/Oauth is commonly used in Vue apps. It may be worth creating an NPM package for this that extends Axios. If you are interested i'd be more than happy to help with the project. There are a few out there but at a cursory review it does not apear they support refresh. – Tim Wickstrom Aug 01 '18 at 20:58
  • 1
    I delegated the logic of differentiating the calls to [this question](https://stackoverflow.com/questions/51646853/how-to-differentiate-identify-calls-in-axios-interceptor). Hope someone will at least throw an idea for sultion. Otherwise, if the problem is solved, I'd be more than happy to provide this interceptor as a package. – Dawid Zbiński Aug 02 '18 at 06:29
  • @TimWickstrom.com if you're still interested, here is the package I created https://www.npmjs.com/package/axios-auth-refresh – Dawid Zbiński Nov 22 '18 at 20:21
  • Nice work man, it looks like its getting some traction. I knew it would! I will work on incorporating it into the project soon. – Tim Wickstrom Nov 26 '18 at 15:54
  • @TimWickstrom.com might still have some bugs, etc., so be careful. Also, I will surely appreciate any contributions ;) – Dawid Zbiński Nov 26 '18 at 16:01
  • 1
    @DawidZbiński This is really handy. Thank you for putting it together. – Daniel Mlodecki Mar 21 '19 at 14:31
  • What is the return Promise.reject(error); doing?? – Pep Jan 20 '21 at 18:40
  • @Pep it's returning the original error if it hasn't been intercepted, so that your original error handling can handle it. – Dawid Zbiński Jan 20 '21 at 19:49
  • @DawidZbiński this is really great and you saved my day.... thanks for the sample code and I used the second approach with axios intercepter – Husni Jabir Oct 27 '21 at 11:25
  • Did something change in axios in regards to this? I am looking to do the same thing and the part that makes new calls with refresh token does not access original promises and original `then` parts. I assume that `Axios.request(error.config);` would always trigger original promise's `then` part? – Robert Jun 24 '22 at 08:05
  • Why is the `error.config.baseURL` set to undefined? – Hubert Formin Jan 05 '23 at 21:45
9

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.

Igor Sukharev
  • 2,467
  • 24
  • 21
7

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;
    });
  }
); 
IVO GELOV
  • 13,496
  • 1
  • 17
  • 26
  • I was missing `return new Promise( (resolve,reject) => {//refresh code}` and I didn't even think to parse `resolve` and `reject` in to the `.then()` function. This answer could use a bit more explanation to your code though. But a solid code piece. –  Jan 16 '19 at 10:57
  • What is the point of `__isRetry` property? I dont' see it exists on AxiosError.config – moze Jul 24 '22 at 13:06
  • @moze `__isRetry` prevents us from entering an endless cycle - it is our own flag to signal to our interceptor that the given AJAX request was automatically generated and we should not try to retry it. – IVO GELOV Jul 25 '22 at 10:48
  • "given AJAX request" - by that You mean the request that needs to be retried after refreshing the token? Still can't see the point of using the flag. `_retry` is only valid for the current error response. How is it possible for an endless cycle to be created? – moze Jul 25 '22 at 14:44
  • Simply remove the setting and checking of `__isRetry` and you will enter the endless cycle as soon as your token expires. "given AJAX request" refers to the argument (named **error** here) that is received by the 2nd callback which is provided to the `interceptors.response.use()` If we get 401 for the first time - we ask a new token and retry the request but if we get 401 for the second time - then we give up and throw an error. – IVO GELOV Jul 26 '22 at 09:11
  • This is totally sufficient. Thanks for the hint of storing the refresh-info in the request from axios itself ;) – nonNumericalFloat Oct 12 '22 at 12:08