最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - AxiosVue - Prevent axios.all() to keep executing - Stack Overflow

programmeradmin6浏览0评论

In my application wile authenticating the user I call the fetchData function. If the user token bee invalid, the application will run axios.all() and my interceptor will return a lot of errors.

How to prevent axios.all() of keep runing after the first error? And show only one notification to the user?

interceptors.js

export default (http, store, router) => {
    http.interceptors.response.use(response => response, (error) => {
        const {response} = error;

        let message = 'Ops. Algo de errado aconteceu...';

        if([401].indexOf(response.status) > -1){
            localforage.removeItem('token');

            router.push({
                name: 'login'
            });

            Vue.notify({
                group: 'panel',
                type: 'error',
                duration: 5000,
                text: response.data.message ? response.data.message : message
            });
        }

        return Promise.reject(error);
    })
}

auth.js

const actions = {
    fetchData({mit, dispatch}) {
        function getChannels() {
            return http.get('channels')
        }

        function getContacts() {
            return http.get('conversations')
        }

        function getEventActions() {
            return http.get('events/actions')
        }

        // 20 more functions calls

        axios.all([
            getChannels(),
            getContacts(),
            getEventActions()
        ]).then(axios.spread(function (channels, contacts, eventActions) {
            dispatch('channels/setChannels', channels.data, {root: true})
            dispatch('contacts/setContacts', contacts.data, {root: true})
            dispatch('events/setActions', eventActions.data, {root: true})
        }))
    }
}

In my application wile authenticating the user I call the fetchData function. If the user token bee invalid, the application will run axios.all() and my interceptor will return a lot of errors.

How to prevent axios.all() of keep runing after the first error? And show only one notification to the user?

interceptors.js

export default (http, store, router) => {
    http.interceptors.response.use(response => response, (error) => {
        const {response} = error;

        let message = 'Ops. Algo de errado aconteceu...';

        if([401].indexOf(response.status) > -1){
            localforage.removeItem('token');

            router.push({
                name: 'login'
            });

            Vue.notify({
                group: 'panel',
                type: 'error',
                duration: 5000,
                text: response.data.message ? response.data.message : message
            });
        }

        return Promise.reject(error);
    })
}

auth.js

const actions = {
    fetchData({mit, dispatch}) {
        function getChannels() {
            return http.get('channels')
        }

        function getContacts() {
            return http.get('conversations')
        }

        function getEventActions() {
            return http.get('events/actions')
        }

        // 20 more functions calls

        axios.all([
            getChannels(),
            getContacts(),
            getEventActions()
        ]).then(axios.spread(function (channels, contacts, eventActions) {
            dispatch('channels/setChannels', channels.data, {root: true})
            dispatch('contacts/setContacts', contacts.data, {root: true})
            dispatch('events/setActions', eventActions.data, {root: true})
        }))
    }
}
Share Improve this question asked Feb 20, 2019 at 0:53 Caio KawasakiCaio Kawasaki 2,9507 gold badges37 silver badges73 bronze badges 6
  • See this answer at Wait until all ES6 promises plete, even rejected promises. You can alternatively substitute an AsyncGenerator and AsyncIterator for .all() see Run multiple recursive Promises and break when requested; see also Jquery Ajax prevent fail in a deferred sequential loop. – guest271314 Commented Feb 20, 2019 at 1:13
  • axio.all() uses Promise.all(). Can you demonstrate Promise.all() continuing execution after first exception or rejected Promise? Why is .catch() not chained to .then() to handle error? – guest271314 Commented Feb 20, 2019 at 1:31
  • axios.all doesn't execute anything, and it cannot "stop" anything. It just builds a promise that waits for other promises. You are calling getChannels(), getContacts() and getEventActions() immediately, they all are already running when you get the first error from them. – Bergi Commented Mar 1, 2019 at 11:03
  • Your best bet will probably be to make one request that checks whether the user token is valid, and only when that succeeds run the others. Instead of relying on the interceptor. – Bergi Commented Mar 1, 2019 at 11:05
  • Hi, I can see that you reopened a bounty on this question. How is my answer not responding to your question? Do you have other requirements that my answer does not fulfill? – Hammerbot Commented Mar 25, 2019 at 10:39
 |  Show 1 more ment

3 Answers 3

Reset to default 8 +25

EDIT: @tony19's answer is much better as it allows to cancel requests still pending after first error, and does not need any extra library.


One solution would be to assign a unique identifier (I will use the uuid/v4 package in this example, feel free to use something else) to all the requests you use at the same time:

import uuid from 'uuid/v4'

const actions = {
    fetchData({mit, dispatch}) {
        const config = {
            _uuid: uuid()
        }

        function getChannels() {
            return http.get('channels', config)
        }

        function getContacts() {
            return http.get('conversations', config)
        }

        function getEventActions() {
            return http.get('events/actions', config)
        }

        // 20 more functions calls

        axios.all([
            getChannels(),
            getContacts(),
            getEventActions()
        ]).then(axios.spread(function (channels, contacts, eventActions) {
            dispatch('channels/setChannels', channels.data, {root: true})
            dispatch('contacts/setContacts', contacts.data, {root: true})
            dispatch('events/setActions', eventActions.data, {root: true})
        }))
    }
}

Then, in your interceptor, you can choose to handle the error a single time using this unique identifier:

export default (http, store, router) => {
    // Here, you create a variable that memorize all the uuid that have
    // already been handled
    const handledErrors = {}
    http.interceptors.response.use(response => response, (error) => {
        // Here, you check if you have already handled the error
        if (error.config._uuid && handledErrors[error.config._uuid]) {
            return Promise.reject(error)
        }

        // If the request contains a uuid, you tell 
        // the handledErrors variable that you handled
        // this particular uuid
        if (error.config._uuid) {
            handledErrors[error.config._uuid] = true
        }

        // And then you continue on your normal behavior

        const {response} = error;

        let message = 'Ops. Algo de errado aconteceu...';

        if([401].indexOf(response.status) > -1){
            localforage.removeItem('token');

            router.push({
                name: 'login'
            });

            Vue.notify({
                group: 'panel',
                type: 'error',
                duration: 5000,
                text: response.data.message ? response.data.message : message
            });
        }

        return Promise.reject(error);
    })
}

Additional note, you could simplify your fetchData function to this:

const actions = {
    fetchData({mit, dispatch}) {
        const config = {
            _uuid: uuid()
        }

        const calls = [
            'channels',
            'conversations',
            'events/actions'
        ].map(call => http.get(call, config))

        // 20 more functions calls

        axios.all(calls).then(axios.spread(function (channels, contacts, eventActions) {
            dispatch('channels/setChannels', channels.data, {root: true})
            dispatch('contacts/setContacts', contacts.data, {root: true})
            dispatch('events/setActions', eventActions.data, {root: true})
        }))
    }
}

The upvoted answer proposes a solution that requires waiting for all responses to plete, a dependency on uuid, and some plexity in your interceptor. My solution avoids all that and addresses your goal of terminating Promise.all() execution.

Axios supports request cancelation, so you could wrap your GET requests with an error handler that cancels the other pending requests immediately:

fetchData({ dispatch }) {
  const source = axios.CancelToken.source();

  // wrapper for GET requests
  function get(url) {
    return axios.get(url, {
        cancelToken: source.token // watch token for cancellation
      }).catch(error => {
        if (axios.isCancel(error)) {
          console.warn(`canceled ${url}, error: ${error.message}`)
        } else {
          source.cancel(error.message) // mark cancellation for all token watchers
        }
      })
  }

  function getChannels() {
    return get('https://reqres.in/api/users?page=1&delay=30'); // delayed 30 secs
  }
  function getContacts() {
    return get('https://reqres.in/api/users?page=2'); // no delay
  }
  function getEventActions() {
    return get('https://httpbin/status/401'); // 401 - auth error
  }

  ...
}

In your interceptor, you'd also ignore errors from request cancellations:

export default (http, store, router) => {
  http.interceptors.response.use(
    response => response,
    error => {
      if (http.isCancel(error)) {
        return Promise.reject(error)
      }

      ...

      // show notification here
    }
}

demo

As an alternative to Axios cancel, you can use Bluebird Promise Cancellation which is simpler.

The advantages of the new cancellation pared to the old cancellation are:

  • .cancel() is synchronous.
  • no setup code required to make cancellation work
  • poses with other bluebird features, like Promise.all

Here is a demo. I've added some logging in axios.get(...).then(...) to track if each call pletes.

Comment out the line promises.forEach(p => p.cancel()) to verify that without cancellation the non-erroring calls will run to pletion.

//for demo, check if fetch pletes 
const logCompleted = (res) => console.log(`Promise pleted, '${res.config.url}'`) 

function getChannels() {
  return axios.get("https://reqres.in/api/users?page=1&delay=5").then(logCompleted)
}
function getContacts() {
  return axios.get("https://reqres.in/api/users?page=2").then(logCompleted)
}
function getEventActions() {
  return axios.get("https://httpbin/status/401").then(logCompleted)
}

Promise.config({ cancellation: true }); // Bluebird config
window.Promise = Promise; // axios promises are now Bluebird flavor

const promises = [getChannels(), getContacts(), getEventActions()];
Promise.all(promises)
  .then(([channels, contacts, eventActions]) => {
    console.log('Promise.all.then', { channels, contacts, eventActions });
  })
  .catch(err => {
    console.log(`Promise.all.catch, '${err.message}'`)
    promises.forEach(p => p.cancel());
  })
  .finally(() => console.log('Promise.all.finally'))
<script src="https://cdnjs.cloudflare./ajax/libs/axios/0.18.0/axios.min.js"></script>
<script src="https://cdn.jsdelivr/bluebird/latest/bluebird.core.min.js"></script>


Why does it work

Promise.all() instead of axios.all()

Looking at this old axios issue Remove axios.all and axios.spread #1042 can see

Axios uses Promise.all under the hood...

and this

axios.all([getUserAccount(), getUserPermissions()])
  .then(axios.spread(function (acct, perms) {
    // Both requests are now plete
}));

can be replaced with this

Promise.all([getUserAccount(), getUserPermissions()])
  .then(function ([acct, perms]) {
    // Both requests are now plete
});

so we can switch to working with Promises directly and still have the same functionality.


Promises fail fast

From MDN we see

Promise.all is rejected if any of the elements are rejected. For example, if you pass in four promises that resolve after a timeout and one promise that rejects immediately, then Promise.all will reject immediately.

so in this pattern

Promise.all(...)
.then(...)
.catch(...);

.catch() will trigger when the first promise fails (contrast to then() which waits until all promises plete).


Composing Promise.all and .cancel()

The pattern is pretty simple, just cancel all the promises in .catch() (which is called on first error).

Ref this question for details Stop other promises when Promise.all() rejects


Substituting Bluebird in Vue store

This is a basic implementation in Vuex.

yarn add bluebird
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";
import Promise from 'bluebird';
Vue.use(Vuex);

Promise.config({ cancellation: true }); // Bluebird config
window.Promise = Promise; // axios promises are now Bluebird flavor

export default new Vuex.Store({
  actions: {
    fetchData({ dispatch }) {
      function getChannels() {
        return axios.get("https://reqres.in/api/users?page=1&delay=5");
      }
      function getContacts() {
        return axios.get("https://reqres.in/api/users?page=2");
      }
      function getEventActions() {  // 401 - auth error
        return axios.get("https://httpbin/status/401");
      }

      const promises = [getChannels(), getContacts(), getEventActions()];
      Promise.all(promises)
        .then(([channels, contacts, eventActions]) => {
          dispatch("channels/setChannels", channels.data, { root: true });
          dispatch("contacts/setContacts", contacts.data, { root: true });
          dispatch("events/setActions", eventActions.data, { root: true });
        })
        .catch(err => {
          promises.forEach(p => p.cancel());
        })
    }
  }
});
发布评论

评论列表(0)

  1. 暂无评论