Axios Interceptors retry original request and access original promise
Solution 1
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
Solution 2
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.
Solution 3
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()
:
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 theresolve
andreject
arguments to AXIOS or otherwise our fresh new promise will be never resolved/rejected.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;
});
}
);
Related videos on Youtube
Tim Wickstrom
I possess a unique blend of business & technical savvy, big-picture vision, and the drive to make that vision a reality
Updated on March 03, 2021Comments
-
Tim Wickstrom about 3 years
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) },
-
Bergi almost 6 yearsYou 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 almost 6 yearsI 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 -
Tim Wickstrom almost 6 yearsI 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.
-
Dawid Zbiński almost 6 yearsWould be nice to know what your
subscribe
action looks like, might help a little. -
Bergi almost 6 years@TimWickstrom Yes, and the only way to run those
then
callbacks is to resolve the promise that theget(…)
call returned. Afaics, the return value of the interceptor callback provides that ability. -
Tim Wickstrom almost 6 years@DawidZbiński great call see updated Questions for subscribe function
-
-
Tim Wickstrom almost 6 yearsIt returns the request that was sent to it.
-
Dawid Zbiński almost 6 yearsI added a working example. You can take a look at it.
-
Tim Wickstrom almost 6 yearsThanks, I will plug in the updated code and give it a try.
-
Dawid Zbiński almost 6 yearsSure. 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.
-
Tim Wickstrom almost 6 yearsGood, reading through the code that was my first question. There could be MANY requests attempted while the access_token is being refreshed.
-
Dawid Zbiński almost 6 yearsSure, 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. -
Tim Wickstrom almost 6 yearsThanks for the quick response. I will plug this into my app and report back my findings
-
Tim Wickstrom almost 6 yearsDawid, you are the man. Works like a champ with some minor updates.
-
Dawid Zbiński almost 6 yearsGreat 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.
-
Tim Wickstrom almost 6 yearsOf course! I had to wait 24 hours before I could award it.
-
Tim Wickstrom almost 6 yearsOne 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 almost 6 yearsLast 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.
-
Dawid Zbiński almost 6 yearsI delegated the logic of differentiating the calls to this question. 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 over 5 years@TimWickstrom.com if you're still interested, here is the package I created npmjs.com/package/axios-auth-refresh
-
Tim Wickstrom over 5 yearsNice work man, it looks like its getting some traction. I knew it would! I will work on incorporating it into the project soon.
-
Dawid Zbiński over 5 years@TimWickstrom.com might still have some bugs, etc., so be careful. Also, I will surely appreciate any contributions ;)
-
Admin over 5 yearsI was missing
return new Promise( (resolve,reject) => {//refresh code}
and I didn't even think to parseresolve
andreject
in to the.then()
function. This answer could use a bit more explanation to your code though. But a solid code piece. -
Daniel Mlodecki about 5 years@DawidZbiński This is really handy. Thank you for putting it together.
-
Pep over 3 yearsWhat is the return Promise.reject(error); doing??
-
Dawid Zbiński over 3 years@Pep it's returning the original error if it hasn't been intercepted, so that your original error handling can handle it.
-
Husni Jabir over 2 years@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
-
Robert almost 2 yearsDid 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 thatAxios.request(error.config);
would always trigger original promise'sthen
part?