Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Global http response error handling in vue/axios with vuex

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
})
like image 206
devman Avatar asked May 05 '20 12:05

devman


4 Answers

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;
like image 118
tao Avatar answered Nov 10 '22 08:11

tao


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.

like image 28
Bordalo Avatar answered Nov 10 '22 10:11

Bordalo


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
});
like image 41
Esteban Martinez Martinez Avatar answered Nov 10 '22 09:11

Esteban Martinez Martinez


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)
    )
  }
  // ...
}
like image 40
strelok2010 Avatar answered Nov 10 '22 09:11

strelok2010