18

In my application wile authenticating the user I call the fetchData function. If the user token become 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({commit, 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})
        }))
    }
}
Caio Kawasaki
  • 2,894
  • 6
  • 33
  • 69
  • See this [answer](https://stackoverflow.com/a/31424853/) at [Wait until all ES6 promises complete, even rejected promises](https://stackoverflow.com/q/31424561/). You can alternatively substitute an `AsyncGenerator` and `AsyncIterator` for `.all()` see [Run multiple recursive Promises and break when requested](https://stackoverflow.com/questions/48349721/run-multiple-recursive-promises-and-break-when-requested/48349837); see also [Jquery Ajax prevent fail in a deferred sequential loop](https://stackoverflow.com/questions/28131082/jquery-ajax-prevent-fail-in-a-deferred-sequential-loop/). – guest271314 Feb 20 '19 at 01: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 Feb 20 '19 at 01: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 Mar 01 '19 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 Mar 01 '19 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 Mar 25 '19 at 10:39
  • Try to add an `.catch()` block at the end of your `axios.all(...)` block. – Kodiak Mar 26 '19 at 13:15

3 Answers3

8

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({commit, 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({commit, 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})
        }))
    }
}
Hammerbot
  • 15,696
  • 9
  • 61
  • 103
4

The upvoted answer proposes a solution that requires waiting for all responses to complete, a dependency on uuid, and some complexity 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.org/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

tony19
  • 125,647
  • 18
  • 229
  • 307
  • 1
    This one is my favorite because it only uses Axios capabilities and because as you stated in here, the upvoted answer waits for all the promises to be completed. However I can't understand how the notification at first fail in the interceptor is supposed to be called. Is it? – Hammerbot Apr 01 '19 at 13:24
  • @Hammerbot When the cancellation token is invoked, it throws an error for all other pending requests on that token, which causes the interceptor's error handler to be called. – tony19 Apr 01 '19 at 15:34
  • 1
    Ho, ok I understand. The notification shows because the first error goes through the interceptor before first cancelation. This should definitely be the validated answer. I'm editing my answer to mention that. – Hammerbot Apr 01 '19 at 17:03
1

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

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

  • .cancel() is synchronous.
  • no setup code required to make cancellation work
  • composes 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 completes.

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

//for demo, check if fetch completes 
const logCompleted = (res) => console.log(`Promise completed, '${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.org/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.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/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 complete
}));

can be replaced with this

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

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 complete).


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.org/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());
        })
    }
  }
});
Community
  • 1
  • 1
Richard Matsen
  • 20,671
  • 3
  • 43
  • 77