顯示具有 Front-end 標籤的文章。 顯示所有文章
顯示具有 Front-end 標籤的文章。 顯示所有文章

2019年9月1日 星期日

[Axios] Interceptor - Refresh access token


 Axios   Interceptor   Refresh Token   Typescript 



Introduction


Axios is the Promise based HTTP client, and is often used in my Vue.js projects.
This article shows how to use Axios Interceptor to refresh the JWT token when the origin token had expired.

The flow (activity diagram) is as following.





The concept and sample codes in based on Axios interceptor by Typescript.
However, they can be apply to other Http Client framework, and of course, also write the code in Javascript.




Environment


axios 0.19.0
vuex 3.0.1
typescript 3.2.1




Implement

          
Vuex: store Access token and Refresh token

I use Vuex store to keep the Access-token (JWT token) and Refresh-token.
And create an Vuex Action to send request to refreshing-token API and then update the new Access-token.

If you are not familiar with Vuex, here are my tutorials about it!


Model: UserToken.ts

export default class UserToken {

  public accessToken: string | null;
  public expiresIn: number | null;
  public refreshToken: string | null;
  constructor(fields?: {
    accessToken?: string,
    expiresIn?: string,
    refreshToken?: string,
  }) {
    if (fields) {
        Object.assign(this, fields);
    }
  }
}




token-store.ts


import { UserToken } from '../classes/apiModel';
import axios from '../modules/axios-module';

export default {
  state: {
    tokens: {} as UserToken,
  },
  getters: {
    tokens: (state: any) => state.tokens,
  },
  mutations: {
    setTokens(state: any, tokens: UserToken) {
      state.tokens = tokens;
    }
  },
  actions: {

    async refreshToken({ commit, state }: { commit: any, state: any}) {
      const isRefreshOk = true;
      if (!state.tokens || !state.tokens.refreshToken) {
        return !isRefreshOk;
      }
      try {
        const url = 'api/Auth/RefreshToken';
        const res: any = await axios.post(state.tokens.refreshToken);
        const userToken = res.data;
        // Mutation
        commit('setTokens', userToken);
        // Return true/false for OK/NG
        return !!userToken ;
      } catch (err) {
        throw err;
      }
    },
  },
};





Axios: Interceptor

We would like Axios to send request with Access token on Http header automatically.
This can be done by setting request’s configuration of interceptor like this,

axios-module.ts

const instance = axios.create();

instance.interceptors.request.use( (config: AxiosRequestConfig) => {

  // Optional headers
  if (store.state.tokenStore && store.state.tokenStore.tokens) {
    const token = store.state.tokenStore.tokens.accessToken;
    config.headers.Authorization = `Bearer ${token}`;
  }

  return config;

}, (error) => {
  return Promise.reject(error);
});




Now we can implement that when receiving 498 response, dispatching “refreshToken” to get the NEW Access-Token, and re-send the failed request.

axios-module.ts

const instance = axios.create();

let isAlreadyFetchingAccessToken = false;

instance.interceptors.response.use( (response: AxiosResponse<any>) => {
  return response;
}, async (error: any) => {
 
  const { config, response: { status } } = error;
  const originalRequest = config;
  let isRefreshOk: boolean = false;

  if (status === 498) {
    if (!isAlreadyFetchingAccessToken) {
      isAlreadyFetchingAccessToken = true;
      isRefreshOk = await store.dispatch('refreshToken');
      isAlreadyFetchingAccessToken = false;
    }

    if (isRefreshOk) {
      const retryOriginalRequest = new Promise((resolve) => {
          resolve(instance(originalRequest));
      });
      return retryOriginalRequest; // Return the resposne from original request with new token
    } else {
      return Promise.reject(error); // Return original response
    }
  } else { // Non 498 response
   return Promise.reject(error);
  }
});

export default instance;



Demo

1.  Send a request to API: [GetAll] and received 498 response
2.  Send a request to API: [RefreshToken]
3.  Re-send a request to API: [GetAll] and OK




Look into the response of refreshing token, we got a new token: “…KAWai_yRG7UXLfYwwFg”




And the new token was exactly what we used in the last request.



Source Code





Reference







2019年8月31日 星期六

[Axios] Interceptor - Error handling


 Axios   Interceptor   Error handling   Typescript 



Introduction


Axios is the Promise based HTTP client, and is often used in my Vue.js projects.
This article shows how to integrate the custom error to handle non-2XX response and timeout request, so that we can show customized message for end-user, not the real error.

The concept and sample codes in based on Axios interceptor by Typescript.
However, they can be apply to other Http Client framework, and of course, write the code in Javascript.


Environment


axios 0.19.0
typescript 3.2.1



Implement

          
(Optional) Set default valid Http Status Codes

The default valid Http Status Codes are >= 200 and < 300.
That means the response with status 200~299 will NOT be rejected by Axios.
You can change the default value by setting axios.defaults.validateStatus,

For example, the 498 response will not be rejected by Axios with the following configuration:

import axios, { AxiosRequestConfig, AxiosInstance, AxiosResponse } from 'axios';

const instance = axios.create();

// Overwrite the ValidateStatus settings
instance.defaults.validateStatus = (status: number) => {
  return (status >= 200 && status < 300) || status == 498;
};

export default instance;



Error handling for rejected Http response

We can create custom error class with custom (and international!) error message for certain rejected response.

MyCustomError.ts


export default class MyCustomError extends Error {
  public url: string;
  public statusCode: number | undefined;
  constructor(message?: string, url?: string, status?: number) {
    super(message);
    this.name = 'MyCustomError';
    this.url = String(url);
    this.statusCode = status;
    this.stack = new Error().stack;
  }
  /**
   * Error msg for server side
   * @memberof HttpRequestError
   */
  public toServerString = (): string => {
    return `[${this.name}][${this.statusCode || '??'}][${this.url}] ${this.message}`;
  }
  /**
   * Error msg for end-user
   * @memberof HttpRequestError
   */
  public toString = (): string => {
    return i18n.tc('error.httpRequestError');
  }
}


Notice that we separate the client-side error message from the server-side message because we don’t want the end-user to get the error’s detail J


Error handling for rejected Http response

Now we can integrate the custom error into the Axios interceptor.

import axios, { AxiosRequestConfig, AxiosInstance, AxiosResponse } from 'axios';
import JL from '@/modules/jsnlogger';
import store from '@/store';
import { MyCustomError } from '../error-model';

const instance = axios.create();

instance.interceptors.response.use( (response: AxiosResponse<any>) => {
  return response;
}, async (error: any) => {
 
  const { config, response: { status } } = error;
  const originalRequest = config;
  const url = originalRequest.url;
 
// Error handling here ...
  // For example, use JsnLog to save server-side log on server side
  const httpErr = new HttpRequestError(error, url, status);
JL.error(httpErr.toServerString());
 
// Reject the error
  return Promise.reject(httpErr);
});

export default instance;



With the above Axios instance, we can easily to catch the custom error and show the custom client-side’s error message as following,

try {
    const url = 'api/xxxx';
    const res: any = await axios.get(url);
    return res.data;
} catch (err) {
    console.log(String(err)); // This will shows the custom client-side msg
}


For example, zh-TW:



en-US:





Error handling for Timeout request

Axios has default timeout as 0 (no timeout).
We can change the default timeout as following,

const instance = axios.create();
instance.defaults.timeout = 3000; // Million seconds


Or by interceptor…


instance.interceptors.request.use( (config: AxiosRequestConfig) => {
 
  config.timeout = 3000; // Million seconds
  return config;

}, (error) => {
  return Promise.reject(error);
});



We can create another custom error model: MyTimeoutError (just like MyCustomError), to handle the timeout error.
Then we can update the Axios interceoptor:

instance.interceptors.response.use( (response: AxiosResponse<any>) => {
  return response;
}, async (error: any) => {
  if (error.code === 'ECONNABORTED') { // Timeout error
    const timeoutErr = new MyTimeoutError(error, error.config.url);
    // Do something…
    return Promise.reject(timeoutErr);
  } else {
    // Handle other err with MyCustomError model ...
  } 
});



Source code





Conclusion

The interceptor allows us to handle and control corresponding error on different responses or scenarios.
So that we can focus on the presentation or business logics when sending a Http request.



Reference