Uncoke
Uncoke

Reputation: 1902

Vue SPA retrieve status code on error codes (not 200) in nested promise

In my VUE components, I use this async method to fetch data from API:

Components:

methods: {
  async fetch() {
    // console.log("##### WAIT ####");
    const { data } = await staffRepository.getItems(this.teamId)
    // console.log("##### END WAIT ####");
    this.staffs = data
  },
},

As you can see I use a custom repository to have a single axios code, this repository is imported in my previous component.

staffRepository:

export default {
  getItems(nationId) {
    return Repository.get(`page/${nationId}`)
  },
}

And finally the main repository having the axios code:

Repository:

import axios from 'axios/index'
const baseDomain = 'https://my end point'
const baseURL = `${baseDomain}`
...
const headers = {
  'X-CSRF-TOKEN': token,
  //  'Access-Control-Allow-Origin': '*', // IF you ADD it add 'allowedHeaders' to ai server config/cors.php
  'X-Requested-With': 'XMLHttpRequest',
  'Content-Type': 'application/json',
  Authorization: `Bearer ${jwtoken}`,
}
export default axios.create({
  baseURL,
  withCredentials: withCredentials,
  headers: headers,
})

This code works very nice when the jwtoken is a valid and NOT EXIPRED token.

The problem is when the token is expired or not found and my laravel 5.8 API returns the status code 401 (or other).

GET https://api.endpoint 401 (Unauthorized)

A good solution could catch the status code in staffRepository, the one having the get method.

MySolution: (not working)

getItems(nationId) {
  return Repository.get(`page/${nationId}`)
    .then(response => {
      console.log(response)
    })
    .catch(error => {
      console.log(error.response.status) // <-- it works!
    })
},

This could be nice because in error case the error in console is 401

But I can't use this solution because I have 2 nested promises: this one and the async fetch() into the component.

How can I fix it still using my repository environment?

Upvotes: 0

Views: 1230

Answers (2)

Andrew Vasylchuk
Andrew Vasylchuk

Reputation: 4779

The best practice solution is to use axios's interceptors:

import axios from "axios";
import Cookies from "js-cookie";

export default (options = {}) => {
  let client = options.client || axios.create({ baseURL: process.env.baseUrl });
  let token = options.token || Cookies.get("token");
  let refreshToken = options.refreshToken || Cookies.get("refreshToken");
  let refreshRequest = null;

  client.interceptors.request.use(
    config => {
      if (!token) {
        return config;
      }

      const newConfig = {
        headers: {},
        ...config
      };

      newConfig.headers.Authorization = `Bearer ${token}`;
      return newConfig;
    },
    e => Promise.reject(e)
  );

  client.interceptors.response.use(
    r => r,
    async error => {
      if (
        !refreshToken ||
        error.response.status !== 401 ||
        error.config.retry
      ) {
        throw error;
      }

      if (!refreshRequest) {
        refreshRequest = client.post("/auth/refresh", {
          refreshToken
        });
      }

      const { data } = await refreshRequest;
      const { token: _token, refreshToken: _refreshToken } = data.content;

      token = _token;
      Cookies.set("token", token);

      refreshRequest = _refreshToken;
      Cookies.set("refreshToken", _refreshToken);

      const newRequest = {
        ...error.config,
        retry: true
      };

      return client(newRequest);
    }
  );

  return client;
};

Take a look at client.interceptors.response.use. Also you should have a refreshToken. We are intercepting 401 response and sending post request to refresh our token, then waiting for a new fresh token and resending our previous request. It's very elegant and tested solution that fits my company needs, and probably will fit your needs too.

To send request use:

import api from './api'

async function me() {
  try {
    const res = await api().get('/auth/me')
    // api().post('/auth/login', body) <--- POST

    if (res.status === 200) { alert('success') }
  } catch(e) {
    // do whatever you want with the error
  }
}

Refresh token: The refresh token is used to generate a new access token. Typically, if the access token has an expiration date, once it expires, the user would have to authenticate again to obtain an access token. With refresh token, this step can be skipped and with a request to the API get a new access token that allows the user to continue accessing the application resources.

Upvotes: 1

Ralph King
Ralph King

Reputation: 1064

I would suggest using the returned promise in your component, to make things more explicit:

methods: {
  fetch() {
    let data = null

    staffRepository
      .getItems(this.teamId)
      .then(data => {
        // do something with data
        this.staffs = data
      })
      .catch(e => {
        // do something with error, or tell the user
      })
  },
},

Edit - this will work perfectly fine, as your method in Repository will return a promise by default if you are using axios.

Try this: API code, where HTTP is an axios instance

export const get = (path: string): Promise<any> => {
  return new Promise((resolve, reject) => {
    HTTP.get(`${path}`)
      .then((response) => {
        resolve(response);
      })
      .catch((error) => {
        reject(handleError(error));
      });
  });
};

// ***** Handle errors *****/

export function handleError(error) {
  if (error.response) {
    const status = error.response.status;

    switch (status) {
      case 400:
        // do something
        break;
      case 401:
        // do something, maybe log user out
        break;
      case 403:
        break;
      case 500:
        // server error...
        break;
      default:
      // handle normal errors here
    }
  }
  return error; // Return the error message, or whatever you want to your components/vue files
}

Upvotes: 1

Related Questions