Axios повторить запрос при ошибке

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.

@liuzhen2008

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

indransyah, ashah023, jasekt, briankeane, edidiway, jsilvestri, mycarrysun, uniquejava, sytler, lobo-tuerto, and 22 more reacted with thumbs up emoji

@rubennorte

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);
});
roblav96, levaleks, briankeane, lichenglu, AustinGreen, NoMemoryError, edingroot, Expl0de, lobo-tuerto, nathan815, and 98 more reacted with thumbs up emoji
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

@mmsmsy

When using it I get «config is undefined». Where should I get it from?

@mmieluch

@mmsmsy This line:

return axios.request(config);

should be:

return axios.request(error.config);
nsarvar, mmsmsy, ViYaYaYa, Noemi-, slavede, rodrigooslp, avlasof, datadidit, Buszmen, bolerovt, and 9 more reacted with thumbs up emoji
jpkontreras, sawich, leandrokanis, and cherniv reacted with hooray emoji

@skyrpex

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

@remmycat

@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!

@NomiasSR

remhume can you give me a hand? I’m having an issue with the same wrong behaviour that you had on your program.

@remmycat

iirc we just manually set the content type to the correct one (in our case application/json), which solved it.

@NomiasSR

@akhrabrov

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

@Flyrell

@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
l-ll

mentioned this issue

Jul 3, 2019

@edmondburnett

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

@mortezashojaei

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

Понравилась статья? Поделить с друзьями:
  • Axios ошибка 401
  • Axios обработка ошибок сервера
  • Axios vuex обработка ошибок
  • Axion ошибка rtc
  • Axioma кондиционеры коды ошибок