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
Building on @Patel Praik’s answer to accommodate multiple requests running at the same time without adding a package:
Sorry I don’t know Vue, I use React, but hopefully you can translate the logic over.
What I have done is created a state variable that tracks whether the process of refreshing the token is already in progress. If new requests are made from the client while the token is still refreshing, I keep them in a sleep loop until the new tokens have been received (or getting new tokens failed). Once received break the sleep loop for those requests and retry the original request with the updated tokens:
const refreshingTokens = useRef(false) // variable to track if new tokens have already been requested
const sleep = ms => new Promise(r => setTimeout(r, ms));
axios.interceptors.response.use(function (response) {
return response;
}, async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// if the app is not already requesting a new token, request new token
// i.e This is the path that the first request that receives status 401 takes
if (!refreshingTokens.current) {
refreshingTokens.current = true //update tracking state to say we are fething new tokens
const refreshToken = localStorage.getItem('refresh_token')
try {
const newTokens = await anAxiosInstanceWithoutInterceptor.post(`${process.env.REACT_APP_API_URL}/user/token-refresh/`, {"refresh": refreshToken});
localStorage.setItem('access_token', newTokens.data.access);
localStorage.setItem('refresh_token', newTokens.data.refresh);
axios.defaults.headers['Authorization'] = "JWT " + newTokens.data.access
originalRequest.headers['Authorization'] = "JWT " + newTokens.data.access
refreshingTokens.current = false //update tracking state to say new
return axios(originalRequest)
} catch (e) {
await deleteTokens()
setLoggedIn(false)
}
refreshingTokens.current = false //update tracking state to say new tokens request has finished
// if the app is already requesting a new token
// i.e This is the path the remaining requests which were made at the same time as the first take
} else {
// while we are still waiting for the token request to finish, sleep for half a second
while (refreshingTokens.current === true) {
console.log('sleeping')
await sleep(500);
}
originalRequest.headers['Authorization'] = "JWT " +
localStorage.getItem('access_token');
return axios(originalRequest)
}
}
return Promise.reject(error);
});
If you don’t want to use a while loop, alternatively you could push any multiple request configs to a state variable array and add an event listener for when the new tokens process is finished, then retry all of the stored arrays.
I wanted to do something similar, except not necessarily with authentication, and to keep retrying after a delay when a particular status code is received. Essentially polling via an Axios interceptor. This was my solution:
import axios from 'axios';
const instance = axios.create({
baseURL: 'http://localhost:5000'
});
const sleepRequest = (milliseconds, originalRequest) => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(instance(originalRequest)), milliseconds);
});
};
instance.interceptors.response.use(response => {
return response;
}, error => {
const { config, response: { status }} = error;
const originalRequest = config;
if (status === 420) {
return sleepRequest(1000, originalRequest);
} else {
return Promise.reject(error);
}
});
export default instance;
https://gist.github.com/edmondburnett/38ed3451de659dc43fa3f24befc0073b
Disclaimer
This is not the best solution, is just a solution, there are probably better and more refined solutions on the web, but this one is simply a very simple one to implement.
Also here we are assuming you are using axios to fetch data from the client.
What’s the use case?
Let’s say that you have your frontend application that consumes some APIs, and also your APIs requires some authentication token, like a JWT token, to be sent at every request, and you got this token after a login screen for example.
And JWT token they generally have an expiration date, it could be a hour, a day, or more (but you shouldn’t use longer than that). Doesn’t matter if here we are talking about a refresh token or the actual token, but at some point the API you are calling might refuse your requests because the token expired.
One way to solve this problem, is to handle it when you do the request in your code, so if you have an error on your request, you just redirect it back to the login screen.
Something like this perhaps:
import axios from 'axios';
const fetchData = async () => {
try {
const { data } = await axios.get('some/endpoint');
return data;
} catch (error) {
// this failed, so let's redirect to the login page
console.log(error);
window.location.href = '/';
}
}
Enter fullscreen mode
Exit fullscreen mode
And the above solution is ok, if you do only one request on your page it could work.
But, this also mean that if you have multiple pages, and maybe in every page you do multiple requests, this strategy becomes a bit cumbersome.
Use axios interceptors instead!
A better and simple way to handle the same problem, in a centrealized way, is to use axios interceptors instead.
With interceptors you can hook to a specific lifecycle of the API call, the request and response, and maybe modify the behaviours of them.
The axios.intercepotrs.request.use(config) function has one argument, which is the configuration of the headers, while the axios.intercepotrs.response.use(response, error) has two, which hooks with the .then, or a successful response, and the .catch, when we get an Error (any status that is not 2xx) as a response.
For example on the example below, we’ll tell to axios to execute the code on every requests we do:
import axios from 'axios';
axios.interceptors.response.use(
response => response,
error => {
window.location.href = '/';
});
Enter fullscreen mode
Exit fullscreen mode
As you see above, we do nothing to the response, but if the error is invoked, we redirect to our login page.
Note: you don’t need to do this on every file, you just need to do it once, like on a configuration file for example.
If you want to have better control, like you want to target only some specific http status codes, let’s say the 401 Unauthorized, you can access to that via the error.response.status, so our code will look like this:
axios.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
window.location.href = '/';
}
});
Enter fullscreen mode
Exit fullscreen mode
Do you want to handle this only for some requests?
Well, then you can also create an axios instance and use that instance only for some calls, for example:
// lib/customAxios.js
export const customAxios = axios.create({
baseURL: 'http://yourcoolapi.com/api',
headers: {
'X-Custom-Header': 'foobar'
}
});
customAxios.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
window.location.href = '/';
}
});
export default customAxios;
// yourcode.js
import customAxios from '/lib/customAxios.js';
const fetchData = async () => {
try {
const { data } = await customAxios.get('some/endpoint');
return data;
} catch (error) {
// this failed, so let's redirect to the login page
console.log(error);
}
}
Enter fullscreen mode
Exit fullscreen mode
Again, this is a very simple use case on how to use axios interceptors, there could be different strategies that works as well or better than this one.
Another one could be to use the request interceptor, check the JWT token even before we actually call the API, and then request a new token, or redirect to the login, or else.
But the one I’ve explained in this post is probably the easiest to grasp and handle.
