Axios Interceptors retry original request and access original promise
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)
},
This question has not received enough attention.
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
12 hours ago
const retryOrigReq = store.dispatch('auth/subscribe').then(token => { originalRequest.headers['Authorization'] = 'Bearer ' + token; return Axios(originalRequest) });
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.
– TimWickstrom.com
11 hours ago
Would be nice to know what your
subscribe
action looks like, might help a little.– Dawid Zbiński
11 hours ago
subscribe
@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
11 hours ago
then
get(…)
2 Answers
2
The key here is to return 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 bind onto the right Promise object, and be executed when the Promise is done. This way you need no extra query for keeping your calls that waited for the refresh.
.then()
refreshing
true
.then()
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 executed in the order they were actually called, you need to pass the callback as a second parameter to the refreshToken()
function, like so.
refreshToken()
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
It returns the request that was sent to it.
– TimWickstrom.com
9 hours ago
I added a working example. You can take a look at it.
– Dawid Zbiński
8 hours ago
Thanks, I will plug in the updated code and give it a try.
– TimWickstrom.com
8 hours ago
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
8 hours ago
Good, reading through the code that was my first question. There could be MANY requests attempted while the access_token is being refreshed.
– TimWickstrom.com
8 hours ago
Why not try something like this ?
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');
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;
});
}
);
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
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
12 hours ago