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;
});
}
);