Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Axios Interceptors retry original request and access original promise

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) }, 
like image 736
Tim Wickstrom Avatar asked Jul 27 '18 18:07

Tim Wickstrom


2 Answers

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

like image 83
Dawid Zbiński Avatar answered Sep 27 '22 23:09

Dawid Zbiński


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.

like image 24
Igor Sukharev Avatar answered Sep 27 '22 23:09

Igor Sukharev