Axios で良い感じにリトライ機能を実装する
こんにちは。ぬこすけです。
axios
は JavaScript 界隈では有名で http 通信をするのに便利なライブラリですが、タイムアウトで通信に失敗した時にリトライする機能はありません。
リトライ機能を追加するためのライブラリ、例えば axios-retry もありますが、簡易的であればライブラリを導入せずともサクッとリトライ機能は実装できるので紹介したいと思います。
実現する機能
この記事で紹介する機能は次のようになります。
- リクエストしてタイムアウトになった時にリトライをする。
- 指定した回数、成功するまでリクエストする。
- リトライする時は指定した時間、間を空けてリクエストする。
- リクエストに成功、または指定した回数分リトライを実行したら、リトライ回数はリセットする。
- URL ごとでリトライ情報を保持する。
- 例えば、
/hoge
と/fuga
にリクエストした時に、/hoge
でタイムアウトエラーになった時には/fuga
のリトライ回数には影響を与えない。
- 例えば、
コード例
説明に入る前に先にコード例を載せておきます。 axios
のバージョンは 1.2.2 です。
(一部処理を簡略化しています。 最後に CodeSandbox で完成形を載せています)
-
axios
にリトライ機能を追加する関数(retryAxios
)
import { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
interface RetrySetting {
readonly maxRetryCount: number;
readonly retryDelay?: number;
}
const retryAxios = (
axiosInstance: AxiosInstance,
{ maxRetryCount, retryDelay = 0 }: RetrySetting
): void => {
const retryCounterMap = new Map();
axiosInstance.interceptors.response.use(
(response) => {
retryCounterMap.delete(response.config.url);
return response;
},
async (error) => {
if (error.code === "ECONNABORTED") {
const { url } = error.config;
const retryCounter = (retryCounterMap.get(url) || 0) + 1;
if (retryCounter > maxRetryCount) {
retryCounterMap.delete(url);
return Promise.reject(error);
}
retryCounterMap.set(url, retryCounter);
return new Promise((resolve, reject) =>
setTimeout(async () => {
try {
const res = await axiosInstance.request(
error.config as AxiosRequestConfig
);
resolve(res);
} catch (e) {
reject(e);
}
}, retryDelay)
);
}
return Promise.reject(error);
}
);
};
export default retryAxios;
-
retryAxios
を使う側のコード
import axios from "axios";
import retryAxios from "./retryAxios";
const axiosInstance = axios.create({
baseURL: "https://hogeeeeeeeeeeeeeeeeee.com",
timeout: 1000
});
retryAxios(axiosInstance, { maxRetryCount: 3, retryDelay: 3000 });
axiosInstance1.get("/fuga");
axiosInstance1.get("/piyo");
いくつかポイントを分けて説明します。
Map
を追加
リトライ情報を保持する const retryCounterMap = new Map();
retryAxios
内で最初に Map
を生成しています。
この Map
はキーにリクエストした URL 、バリューにリトライした回数を記憶します。
retryCounterMap
があることで URL ごとでリトライを管理できるわけです。
余談ですが、キーとバリューの管理なら Object でもできます。
const retryCounterMap = {};
ですが、頻繁に読み書きを行うなら Map
の方がパフォーマンス的にベターです。
レスポンスをカスタマイズしてリトライを実装
axiosInstance.interceptors.response.use(
(response) => {
retryCounterMap.delete(response.config.url);
return response;
},
async (error) => {
if (error.code === "ECONNABORTED") {
const { url } = error.config;
const retryCounter = (retryCounterMap.get(url) || 0) + 1;
if (retryCounter > maxRetryCount) {
retryCounterMap.delete(url);
return Promise.reject(error);
}
retryCounterMap.set(url, retryCounter);
return new Promise((resolve, reject) =>
setTimeout(async () => {
try {
const res = await axiosInstance.request(
error.config as AxiosRequestConfig
);
resolve(res);
} catch (e) {
reject(e);
}
}, retryDelay)
);
}
return Promise.reject(error);
}
);
axios.interceptors.response.use
を使うことでレスポンスをフックしてカスタマイズすることができます。
第一引数に正常にレスポンスを受け取った時の処理、第二引数にエラーになった時の処理を指定します。
まず第二引数にエラーになった時の処理を見てみましょう。
async (error) => {
if (error.code === "ECONNABORTED") {
const { url } = error.config;
const retryCounter = (retryCounterMap.get(url) || 0) + 1;
if (retryCounter > maxRetryCount) {
retryCounterMap.delete(url);
return Promise.reject(error);
}
retryCounterMap.set(url, retryCounter);
return new Promise((resolve, reject) =>
setTimeout(async () => {
try {
const res = await axiosInstance.request(
error.config as AxiosRequestConfig
);
resolve(res);
} catch (e) {
reject(e);
}
}, retryDelay)
);
}
return Promise.reject(error);
}
エラーコードが ECONNABORTED
(タイムアウト)になった時にリトライを実行します。
簡単に次のような処理になります。
-
url
をキーにMap
(retryCounterMap
)に保持しているリトライ回数を引き当てて 1 加算。 - 指定した Max のリトライ回数を超えたら、リトライ情報をリセット。
- 指定した Max のリトライ回数内だったら、指定した時間分遅延して再度リクエスト
次に正常にレスポンスを受け取った時の処理を見てみましょう。
(response) => {
retryCounterMap.delete(response.config.url);
return response;
},
正常にレスポンスを受け取れた場合は、Map
に保持しているリトライ回数をリセットします。
こうすることで再度同じ URL へのリクエストでタイムアウトが起きた時にもリトライ処理を走らせることができます。
retryAxios
を使う側はリトライ回数の上限などを指定
const axiosInstance = axios.create({
baseURL: "https://hogeeeeeeeeeeeeeeeeee.com",
timeout: 1000
});
retryAxios(axiosInstance1, { maxRetryCount: 3, retryDelay: 3000 });
axiosInstance.get("/fuga1");
axiosInstance.get("/piyo1");
最初に axios.create
をすることで、 baseURL
など axios
に設定を加えた上でインスタンス化します。
このインスタンスに対して、 retryAxios
でリトライ回数の上限やリトライする際の遅延時間を指定してリトライ機能を拡張します。
CodeSandbox の完成形
完成形のコードを貼っておきます。
CodeSandbox の console を見ると、リクエストする URL ごとにリトライをしていることがわかります。
※リトライはタイムアウトではなく単純にエラーになった時に実行するように変更はしています。
※特に try~catch
でエラーハンドリングはしていないので画面はエラーが表示されます。
最後に
ライブラリは使わず、 axios
を拡張してリクエストする URL ごとでリトライする実装を紹介しました。
なお、今回のコードを考える上で次の記事が参考になりました!
この記事を元に、
- グローバルではなくリクエストする URL ごとでリトライを管理する。
- 例えば、
/hoge
と/fuga
にリクエストした時に、/hoge
でタイムアウトエラーになった時には/fuga
のリトライ回数には影響を与えないようにする。
- 例えば、
-
axios
の API インターフェースを壊さずに機能拡張する。-
AxiosRequestConfig
にプロパティを追加 をしないように機能拡張する。
-
- リトライに遅延時間を追加する。
- リクエストに成功した時などしかるべきタイミングでリトライ情報をリセットする。
など改良を加えさせていただきました。
ぜひ皆さんの参考になればと思います。良い JavaScript ライフを!by ぬこすけ
Discussion