Global http response error handling in vue/axios with vuex
Asked Answered
E

4

8

I'm trying to fix a behavior in my VueJS SPA wherein a limbo state arises. The app doesn't know the JWT has already expired and therefore presents itself as if the user is still logged in. This can happen after hibernation, for example.

These users can keep on making any request to the API, but end up with a 401 response (and correctly so).

I'd like to have a global handler for 401 responses. (This would be: "clear everything user-related from vuex and present the page as if the user was a guest, with login form popup, etc.") Otherwise, I would have to write a 401 handler for EVERY request.

I can add response interceptors to axios, and they work fine. These interceptors don't have access to Vuex (or Vue), though.

Whenever I try to import Vuex or Vue into my Axios, I get circular dependencies (of course) and everything breaks.

If I just throw/return the error, I still have to handle it separately on every request. How can I dispatch methods on this.$store from within an axios interceptor?

The Axios file contains an export default class API that is added to Vue globally in main.js:

import api from 'Api/api'
// ...
Vue.prototype.$http = api

I had thought there has to be a way to access Vue from $http, since it's a global instance method. But I appear to be mistaken?

Code

main.js

// ...
import api from 'Api/api'
// ...
Vue.prototype.$http = api

new Vue({
  el: '#app',
  router,
  store,
  template: '<App/>',
  components: { App },
  vuetify: new Vuetify(opts),
});

api.js

import Client from './ApiClient'

const apiClient = new Client({ basePath: process.env.VUE_APP_API_URL })

const api = {
  get(url) {
    return apiClient._get(`${basePath}/${url}`)
  },
  post(url, data) {
    return apiClient._post(`${basePath}/${url}`, data)
  },
  // ...
}
export default api

ApiClient.js

const axios = require('axios')

const errorHandler = (error) => {
  if (error.response.status === 401) {
    store.dispatch('user/logout') // here is the problem
  }
  return Promise.reject({ ...error })
}


export default class API {
  constructor(options) {
    this.options = Object.assign({ basePath: '' }, options)
    this.axios = axios.create({ timeout: 60000 })
    this.axios.interceptors.response.use(
      response => response,
      error => errorHandler(error)
    )
  }
  // ...
}

Importing store in ApiClient.js results in a dependency cycle: I assume because I'm importing Vue in it?

store.js

import Vue from 'vue'
import Vuex from 'vuex'
import PersistedState from 'vuex-persistedstate'
import CreateMutationsSharer from 'vuex-shared-mutations';
import SecureLS from 'secure-ls';
// import modules

Vue.use(Vuex);
const ls = new SecureLS({ encodingType: 'aes' });

export default new Vuex.Store({
  // options
})
Eusebioeusebius answered 5/5, 2020 at 12:48 Comment(1)
the client will never know if the token is still validEusebioeusebius
L
3
conf
import Axios from 'axios'
import IdentityProxy from './IdentityProxy.js'
import UsuariosProxi from './UsuariosProxi'
import ZonasProxi from './ZonasProxi'

//axios
Axios.defaults.headers.common.Accept='application/json'
//Axios.defaults.headers.common['Access-Control-Allow-Origin'] = '*';

Axios.interceptors.request.use(
    config => {
        let token = localStorage.getItem('access_token');

        if(token){
            config.headers= {
                'x-access-token': `${token}`
            }
        }
        return config;
    },
    error => Promise.reject(error)
);
Axios.interceptors.response.use(
    response => response,
    error => {
      if (error.response.status===403||error.response.status===401) {
        localStorage.removeItem('access_token');
        window.location.reload(true);
      }
   
      return Promise.reject(error);
    }
  );
let url=null

if(localStorage.getItem("config")!==null){
    let config = JSON.parse(localStorage.getItem("config"))
    url = config
}

console.log(url)
export default{
    identityProxy: new IdentityProxy(Axios, url),
    _usuarioProxi: new UsuariosProxi(Axios, url),
    _zonasProxi: new ZonasProxi(Axios, url),
}
//
export default class IdentityProxy{

    constructor(axios,url){
    this.axios = axios;
    this.url =url;
    }

    register(params){
        return this.axios.post(this.url+'/identity/register',params)
    }

    login(params){
        
        return this.axios.post(this.url+'/auth/signin',params)
    }
}
//
export default class UsuariosProxi{
    constructor(axios,url){
    this.axios = axios;
    this.url =url;
    }

    /* getAll(){
        return this.axios.get(this.url+'/users')
    } */
    getAll(page, take) {
        return this.axios.get(this.url + `/users?page=${page}&take=${take}`);
    }
    create(params) {
        return this.axios.post(this.url + '/auth/signup', params);
    }

    get(id) {
        return this.axios.get(this.url + `/users/${id}`);
    }
    update(id, params) {
        return this.axios.put(this.url + `/users/${id}`, params);
    }

    remove(id) {
        return this.axios.delete(this.url + `/users/${id}`);
    }
    //-----APARTE SOLO TRAE LISTA DE ROLES--------
    getRoles() {
        return this.axios.get(this.url + '/users/newrol');
    }
}
//st
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const state = {
    user:null
}
export default new Vuex.Store({
    state
});
Leathern answered 6/8, 2021 at 12:21 Comment(0)
A
2

main.js:

import store from './store';

const Instance = new Vue({
  store,
  ...
})

export const { $store } = Instance;

Now you can import { $store } from '@/main.js' anywhere you want. And it's going to be the same instance you have mounted in your app, not a new Vuex.Store({}) (which is what ./store exports, each time you import it somewhere else).

You can export the same way anything else you might want to use in services, tests, helpers, etc... I.e:

export const { $store, $http, $bus, $t } = Instance;
Aguiar answered 2/6, 2020 at 12:54 Comment(3)
Doesn't this approach also create a dependency cycle, but this time starting in the main.js file?Antenatal
@mutsa, normally, it doesn't. If you have such a case and you can't find a solution around it, please post it as a different question.Aguiar
If I understood your suggestion correctly, it is supposed to import main.js into ApiClient.js to use the store on it, and this will create another "dependency cycle" because of main.js -> api.js -> ApiClient.js -> main.js. I managed to solve this specific problem with another approach that I'll post. @Aguiar tks for your timeAntenatal
G
1

What about direct import your store to ApiClient.js? Something like

const axios = require('axios')
import store from 'path/to/store'

const errorHandler = (error) => {
if (error.response.status === 401) {
  store.dispatch('user/logout') // now store should be accessible
}
  return Promise.reject({ ...error })
}


export default class API {
  constructor(options) {
    this.options = Object.assign({ basePath: '' }, options)
    this.axios = axios.create({ timeout: 60000 })
    this.axios.interceptors.response.use(
      response => response,
      error => errorHandler(error)
    )
  }
  // ...
}
Gault answered 5/5, 2020 at 14:2 Comment(3)
that results in a dependency cycle and an overflow. i'm not really sure why though. i've imported Vue in store.js, might that be the problem? gonna add store.js to my questionEusebioeusebius
Which tool you use for building an app? I have the same store file (with Vue imported) and it's imports to my api.js without any problems, can you please show which error you have?Gault
i think i found the problem thanks to double checking now that you said it works for you. i had an endless loop of requests, but those seem to have come from the user/logout action, which sends another request to the API, telling it to discard the JWT. which... of course, doesn't work if you're not logged in :D i think that's it. gonna dig a little deeper and make sure. if i don't post any update here in the near future, it will have worked and your answer will be accepted, thanks :)Eusebioeusebius
A
1

Base on these thread I was able to manage a solution for my needs:

main.js

import api, {apiConfig} from 'Api/api'
apiConfig({ store: $store });

ApiClient.js

let configs = {
  store: undefined,
};
const apiConfig = ({ store }) => {
  configs = { ...configs, store };
};
export default api;
export { apiConfig };

This way the api.js file will require a configuration that can later be expanded.

Antenatal answered 7/1, 2021 at 12:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.