axios-module: Globally capture errors

Version

v5.1.0

Reproduction link

https://axios.nuxtjs.org/extend

Steps to reproduce

Follow setting axios from nuxt documentary.

What is expected ?

Whenever we catch the error inside $axios.onError we should redirect to the given route. I could handle the error on each request by catching it, but as far as we got global handler we should be able to catch it in one place.

What is actually happening?

It is throwing NuxtServerError.

 [Axios] Response error: Request failed with status code 401                                                                                                                                                         
 09:59:32

 at createError (node_modules/axios/lib/core/createError.js:16:15)
 at settle (node_modules/axios/lib/core/settle.js:18:12)
 at IncomingMessage.handleStreamEnd (node_modules/axios/lib/adapters/http.js:201:11)
 at IncomingMessage.emit (events.js:202:15)
 at IncomingMessage.EventEmitter.emit (domain.js:481:20)
 at endReadableNT (_stream_readable.js:1132:12)
 at processTicksAndRejections (internal/process/next_tick.js:76:17)

Additional comments?

I would like to add my code to describe more precisely situation:

I have registered axios module and extended it:

plugins: [
    '~plugins/axios',
],

modules: [
    '@nuxtjs/axios',
],

The plugin:

const EXPIRED_TOKEN_MESSAGE = 'Expired JWT Token';

export default function ({
    $axios, redirect, app, store,
}) {
    $axios.setHeader('Content-Type', 'application/json');
    $axios.setHeader('Accept', 'application/json');

    $axios.onRequest((config) => {
        console.log(`Making request to ${config.url}`);
    });
    
    $axios.onError((error) => {
        const { response: { data: { message } } } = error;

        if (message === EXPIRED_TOKEN_MESSAGE) {
            store.dispatch('authentication/logout');
        }

        store.dispatch('alerts/addAlert', { type: 'error', message });
    });
}

The store:

logout({ commit }) {
    Cookie.remove('jwt');
    commit('setUser', null);
    commit('data/clearStorage', {}, { root: true });

    this.$router.push({ name: 'index' });
},
<div align="right">This bug report is available on Nuxt community (#c235)</div>

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 2
  • Comments: 19 (8 by maintainers)

Commits related to this issue

Most upvoted comments

While it is not really recommended to globally suppress errors what you can do is:

export default function ({ $axios }) {
  $axios.onError((error) => {
    return { error, data: {} }
  })
}

It will cause if $axios requests failing, return an empty object for data and error is available in reponse object but nothing throws anymore.

Hi @marcvangend It is because you need to resolve response in error interceptor otherwise it will be simply just intercepted (for logging or show an alert). So this fixes issue: (sandbox)

  client.onError((err) => {
    return Promise.resolve(false); <--
  });

If you wish to update docs noticing this, would be awesome 😃

Hi. You have to handle the error. Maybe returning an empty object at the end of onError handler.

Ah, excellent, thank you @pi0! At least it’s reassuring to see that I was pretty close 😃

So if I understand correctly:

  • Returning a resolved promise here is like telling Nuxt/Axios “We have sufficiently handled this error, please carry on as if everything is OK”.
  • If you don’t, the error wil propagate to the place where the request was initiated.
  • If the error remains uncaught there, it will trigger the Axios error page, even if you called the Nuxt error() function earlier. (That last bit it what caused my confusion, I guess.)

I will try to submit a PR for the docs.

i solved this problem with error catcher in fetch request

async fetch() {
    try {
      await this.getLogs()
    } catch (error) {
       if (process.server) {
       const code = this.$nuxt.context.res.statusCode
       if (code === 401) {
         this.$store.dispatch('auth/logout')
       }
      }
    }
  },

Sorry for bumping a closed issue, but I’m seeing similar issues and I’d really like to understand how things are supposed to work.

I prepared a CodeSandbox, I hope @pi0 you are able to have a look: https://codesandbox.io/s/floral-voice-flh6f?file=/pages/fail.vue. It uses a plugin $apiClient to handle api calls with Axios, the structure of which is based on this post: Organize and decouple your API calls in Nuxt.js. This plugin also adds error handling using the onError interceptor. Two pages (working.vue and fail.vue) use this apiClient to retrieve data. One works but the other uses an incorrect id on purpose, causing a 404 response from the api server. I expect this error to be handled by the onError hook.

Now my question… The only way I was able to get the error handling to work, was by adding .catch() in my asyncData hook like this:

  asyncData({ $apiClient }) {
    return $apiClient.mountains
      .get("aconcagua")
      .then((response) => {
        return { response };
      })
      .catch(() => false);
  },

As you can see the .catch(() => false); line doesn’t do much, but without it, the Axios error handler doesn’t kick in. Is this how it’s supposed to be? How does this work? Shouldn’t the onError interceptor take simply care of things, regardless of what happens in asyncData?

Thank you for your time and support.

I found what is the problem: nuxtServerInit is running into infinity loop. I do have authentication copy pasted from Nuxt guide the only thing I have added is Axios interceptor helpers based on newest approach.

Axios.js plugin:

     const EXPIRED_TOKEN_MESSAGE = 'Expired JWT Token';
    
    export default function ({
        $axios, redirect, app, store,
    }) {
        $axios.setHeader('Content-Type', 'application/json');
        $axios.setHeader('Accept', 'application/json');
    
        $axios.onRequest((config) => {
            config.headers.JWTAuthorization = `Bearer ${store.state.authentication.jwt}`;
        });
    
        $axios.onError((error) => {
            const { response: { data: { message } } } = error;
    
            if (message === EXPIRED_TOKEN_MESSAGE) {
                store.dispatch('authentication/logout');
                $axios.defaults.headers.common.JWTAuthorization = null;
            }
    
            store.dispatch('alerts/addAlert', { type: 'error', message });
    
            return Promise.reject(error.response);
        });
    }

Vuex store authentication actions:

    export default {
        async authenticateUser({ commit, dispatch }, { data }) {
            await this.app.$axios.$post('login', data).then(({ token }) => {
                this.app.$axios.setHeader('JWTAuthorization', `Bearer ${token}`);
                Cookie.set('jwt', token);
                commit('setAction', { key: 'jwt', value: token });
                dispatch('getUser');
            }).catch(e => console.log(e));
        },
        async getUser({ commit }) {
            await this.app.$axios.$get('profile').then((user) => {
                commit('setAction', { key: 'user', value: user });
                this.$router.push({ name: 'dashboard' });
            }).catch(e => console.log(e));
        },
        logout({ commit }) {
            Cookie.remove('jwt');
            commit('setAction', { key: 'user', value: null });
            commit('setAction', { key: 'jwt', value: null });
            commit('data/clearStorage', {}, { root: true });
        },
    };

Vuex store authentication state:

    const state = () => ({
        user: null,
        jwt: null,
    });
    
    export default state;

And the two middleware:

authenticated.js added into default layout

    export default async function ({ store, redirect }) {
        if (!store.state.authentication.user) {
            await store.dispatch('authentication/getUser');
        }
        if (!store.state.authentication.jwt) {
            return redirect('/');
        }
    }

notAuthenticated added into login layout

    export default function ({ store, redirect }) {
        if (store.state.authentication.jwt) {
            return redirect('/dashboard');
        }
    }

Expected action:

When the axios onError interceptor is hit it should logout user.

What is happening:

nuxtServerInit is keep running at the infinity loop - even when cookies are removed, the seems to be passed into req.headers.cookie.

Any approach for my case ? I would be grateful @pi0

It is only occurring at the server side when page is refreshed and token invalidated - when we are in the app, after token invalidates the behaviour is correct.

It works as expected. Promise.reject means also throwing an error (promise chain).

    $axios.onError((error) => {
        const { response: { data: { message } } } = error;

        if (message === EXPIRED_TOKEN_MESSAGE) {
            store.dispatch('authentication/logout');
        }

        store.dispatch('alerts/addAlert', { type: 'error', message });

        return false
    });

As this is a global handler, you have to check response when using.

async asyncData({ $axios }) {
   const res = $axios.$get('...')
   if (!res) { 
      // An error occured
   }
}