Axios interceptorsでアクセストークンのリフレッシュを共通処理として実装する

# はじめに
Axiosは、HTTP通信を実行するためにJavaScriptで記述されたPromiseベースのHTTPクライアントです。インターセプターと呼ばれる強力な機能が1つあります。
Axios interceptorsを使用すると、リクエストやレスポンスが宛先に到達する前に、リクエストやレスポンスが宛先に到達する前に、コードを実行したり、リクエストやレスポンスを変更したりできます。

今回は認証が必要なエンドポイントにリクエストを行った際に、アクセストークンの有効期限が切れてエラーが返ってきた場合に、トークンのリフレッシュを行い、再度エラーが返されたエンドポイントにリクエストを行う処理をAxios interceptorsを使って実装します。

## この記事で説明する範囲
今回はAxios、アクセストークンなどの詳細な説明は省きます。またアクセストークンを保持する場所などの認証に関する説明も省きます。（Axios interceptorsで行いたい目的は変わらない為）
またheaderの部分の実装などはご利用の環境に合わせてください。

## 1. Axiosにinterceptorsを追加する
以下のように実装すると、1回目のリクエストのレスポンスが返ってきた際に、成功と失敗をハンドリングすることができます。

```axios.js
const axios = Axios.create({
   baseURL: 'http://api.localhost',
   headers: {
      'Content-Type': 'application/json',
       Authorization: 'Bearer' + token
   },
   responsetype: 'json'
})

 axios.interceptors.response.use(
   response => {
      // 成功時はレスポンスを呼び出し元に返す
      return response
   },
   function(error) {
      // エラー時
   }
)
```

## 2. 401のステータスコードなのか・リトライかをチェックする
本来アクセストークンのリフレッシュを行いたいのは、はじめにの部分で上述しましたが、認証が必要なAPIにリクエストを行った際に、アクセストークンの有効期限切れでエラーが起きた場合です。なのでエラー時にステータスコードを判定します。

```axios.js
   function(error) {
      // エラー時
      if (error.response.status === 401) {}
   }
```

しかしこの判定式の中で、アクセストークンのリフレッシュを行うAPIにリクエストを行うと、リフレッシュが失敗した場合に永遠に処理が続いてしまうことになります。なので、リトライかどうかを条件式に追加し、リトライだった場合はPromiseでエラーオブジェクトを返します

```axios.js
 let isRetry = false
 axios.interceptors.response.use(
   response => {
      // 成功時はレスポンスを呼び出し元に返す
      return response
   },
   function(error) {
      // エラー時
      const originalRequest = error.config
      if (error.response.status === 401 && !isRetry) {
          isRetry = true
      } else {
          return Promise.reject(error)
      }
   }
)

```

## 3. リフレッシュを行うエンドポイントにリクエストを行う
ここではリフレッシュを行うAPIに対し、リクエストを送ることが可能な場合の処理を記述します。
ここでは成功時にリフレッシュされたアクセストークンをCookieにセットし再度失敗したリクエストを行っています。
失敗時にはログイン画面に遷移させる場合などが多いのでしょうか、そのように実装しています。

```axios.js
return axios
   .post('refresh')
       .then(res => {
           if (res.status === 200) {
               // set token to access_token in Cookie
               console.log('refresh success')
               console.log(res.data.access_token)
               Cookies.set('access_token', res.data.access_token, {
                   expires: res.data.expires_in / 1440
               })
               // 3) return originalRequest object with Axios.
               console.log(Cookies.get('access_token'))
                   return axios(originalRequest)
               }
       })
       .catch(() => {
           console.log('refresh error')
           router.push({ name: 'top' })
           return Promise.reject(error)
       })
```


## 全体
全体はこのようになります。

```aixos.js
const axios = Axios.create({
   baseURL: 'http://api.localhost',
   headers: {
      'Content-Type': 'application/json',
       Authorization: 'Bearer' + token
   },
   responsetype: 'json'
})

let isRetry = false
axios.interceptors.response.use(
   response => {
      // 成功時はレスポンスを呼び出し元に返す
      return response
   },
   function(error) {
      // エラー時
      const originalRequest = error.config
      if (error.response.status === 401 && !isRetry) {
          isRetry = true
          return axios
              .post('refresh')
                  .then(res => {
                      if (res.status === 200) {
                          // set token to access_token in Cookie
                          console.log('refresh success')
                          console.log(res.data.access_token)
                          Cookies.set('access_token', res.data.access_token, {
                          expires: res.data.expires_in / 1440
                          })
                          // 3) return originalRequest object with Axios.
                          console.log(Cookies.get('access_token'))
                          return axios(originalRequest)
                      }
                  })
                  .catch(() => {
                      console.log('refresh error')
                      router.push({ name: 'top' })
                      return Promise.reject(error)
                  })
     } else {
          // 認証以外のエラーで失敗した場合など
          return Promise.reject(error)
     }
   } 
)