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.
In my interceptors, I wish to catch a specific error (say, token expired)
And then I’ll run some method to refresh the token.
After that resolves, I wish axios to retry the previous request (with the same result handler).
Do I need to implement this logic manually? Or is there a retry or something similar that allows me to do it directly?
Thanks
You can do that with an interceptor that handles authentication errors. Something like:
axios.interceptors.response.use(null, (error) => { if (error.config && error.response && error.response.status === 401) { return updateToken().then((token) => { error.config.headers.xxxx <= set the token return axios.request(config); }); } return Promise.reject(error); });
alfonmga, orlando, ljcastroLemons, EdisonArango, puranjayjain, ecdeveloper, LukePeters, akhrabrov, sawich, birnbuazn, and 12 more reacted with hooray emoji
uriel-shimony, daitonaaa, h2soheili, cherniv, andrewdcato, AmirHMousavi, ptupi, rafaelbpa, mattia-beta, cyberz, and 2 more reacted with heart emoji
akhrabrov, daitonaaa, h2soheili, ptupi, rafaelbpa, mattia-beta, cyberz, and piotr-gawlowski reacted with rocket emoji
This was referenced
Aug 12, 2017
When using it I get «config is undefined». Where should I get it from?
@mmsmsy This line:
return axios.request(config);
should be:
return axios.request(error.config);
jpkontreras, sawich, leandrokanis, and cherniv reacted with hooray emoji
Be careful if you’re using the transformRequest
option: the data from error.config
is already transformed into a string.
For example, the code from @rubennorte would crash with this setup:
// Lets use qs to stringify our request. this.$axios = axios.create({ transformRequest: [ data => qs.stringify(data), ], }); this.$axios.interceptors.response.use(null, (error) => { if (error.config && error.response && error.response.status === 401) { return updateToken().then((token) => { error.config.headers.xxxx <= set the token // Here, the request data will be double stringified with qs.stringify, // potentially leading to 422 responses or similar. return this.$axios.request(config); }); } return Promise.reject(error); });
A solution to this problem is not to transform the request data if it’s already a string. For example:
this.$axios = axios.create({ transformRequest: [ data => (isString(data) ? data : qs.stringify(data)), ], });
@skyrpex Thanks a lot! Your answer brought us on the right track on a long and complicated bug hunt!
It seems the default transformRequest handles transformation like in your example, but axios notices the data is a string and will set the content type to text/plain
instead of application/json
which lead in our case to our API sending back a 400 error.
This was a hard bug to catch!
remhume can you give me a hand? I’m having an issue with the same wrong behaviour that you had on your program.
iirc we just manually set the content type to the correct one (in our case application/json
), which solved it.
You can do that with an interceptor that handles authentication errors. Something like:
axios.interceptors.response.use(null, (error) => { if (error.config && error.response && error.response.status === 401) { return updateToken().then((token) => { error.config.headers.xxxx <= set the token return axios.request(config); }); } return Promise.reject(error); });
If i have 10 parralel requests, axios send 10 updateToken requests.
That may cause «ECONNRESET» error.
Maybe u have solution for this case?
P.S find solution here https://gist.github.com/mkjiau/650013a99c341c9f23ca00ccb213db1c
@akhrabrov I made a simple package if you’d like to use it -> axios-auth-refresh. I’d be more than happy to see the contributions if you’re missing some of the functionalities.
l-ll
mentioned this issue
Jul 3, 2019
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
You should also use below code for prevent crash in post requests :
if(error.config.data) error.config.data = JSON.parse(error.config.data)
axios
locked and limited conversation to collaborators
May 22, 2020
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
Перехват запросов
Вы можете перехватывать запросы или ответы до того, как они будут then
или catch
.
// Добавляем перехват запросов
axios.interceptors.request.use(function (config) {
// Здесь можете сделать что-нибудь с перед отправкой запроса
return config;
}, function (error) {
// Сделайте что-нибудь с ошибкой запроса
return Promise.reject(error);
});
// Добавляем перехват ответов
axios.interceptors.response.use(function (response) {
// Любой код состояния, находящийся в диапазоне 2xx, вызывает срабатывание этой функции
// Здесь можете сделать что-нибудь с ответом
return response;
}, function (error) {
// Любые коды состояния, выходящие за пределы диапазона 2xx, вызывают срабатывание этой функции
// Здесь можете сделать что-то с ошибкой ответа
return Promise.reject(error);
});
Если вам нужно, вы можете удалить перехватчик
const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);
Вы можете добавить перехватчики в пользовательский экземпляр axios.
const instance = axios.create();
instance.interceptors.request.use(function () {/*...*/});
В response
объект в перехватчике Axios содержит config
объект. ( Смотрите здесь)
Вы можете использовать это, чтобы повторно инициировать запрос с исходной конфигурацией.
Пример:
axios.interceptors.response.use((response) => {
return response;
}, (error) => {
if (error.response.status === 429) {
// If the error has status code 429, retry the request
return axios.request(error.config);
}
return Promise.reject(error);
});
Чтобы использовать действие Vuex внутри обратного вызова перехватчика, вы можете сначала определить хранилище как переменную, а затем вызвать функцию диспетчеризации внутри обратного вызова. Как это:
const store = new Vuex.Store({
// define store...
})
axios.interceptors.response.use((response) => {
return response;
}, (error) => {
if (error.response.status === 429) {
store.dispatch("YOUR_ACTION");
return axios.request(error.config);
}
return Promise.reject(error);
});
export default store;