toshi-toma blog

主にフロントエンド、作業ログあとは色々なメモ ✍️ 🍅

axiosで二重リクエストを防ぐ

二重リクエス

フロントエンドからのAPIリクエストにおいて、二重リクエストが問題になることがあります。

例えば、作成ボタンを素早くクリックした際に、何も考慮してないと、データが二重に登録される可能性があります。

バックエンドでそういったことの対策が行われていない場合、フロントエンドで対策しておくことになります。

フロントエンドでの対策として、送信中はボタンをdisabledにするなどもありますが、今回はAPIクライアントで二重にリクエストを送信するのを防ぐ方法について書きます。

axiosで二重リクエストを防ぐ

フロントエンドのAPIクライアントとして、axiosを使っているケースが多いです。

github.com

以下の記事を参考にして、二重リクエストを防ぐAPIクライアントを実装しました。

medium.com

axiosがCancellationに対応しているからこそ対応できています。

実装内容

対応方法は以下の通りです

  • 送信中のリクエスト内容を保存しておく送信中リストを用意する
  • リクエスト送信時に、リクエストの内容を送信中リストに保存する
    • 保存する際のキーは、リクエストURLやメソッド、パラメータ、bodyを文字列化したもの
    • レスポンスが返ってきたら、送信中リストから消す
  • リクエストを保存する前に、送信中リストをチェックして、同じ内容が存在していたら、キャンセルする

axiosインスタンスを作成

まず、axiosインスタンスを作成します。アプリケーションのAPIリクエストは全て、以下のaxiosインスタンスを使って行うようにします。

import axios from "axios";
const axiosInstance = axios.create({
  baseURL: "https://xxxxx/api",
  headers: {
    //
  },
});

リクエストを保存して、二重リクエストならキャンセル

axiosInstance.interceptors.request.useというAPIでリクエスト送信前に任意の処理を実行できます。

// リクエスト用
axiosInstance.interceptors.request.use(
  (request) => {
    request.cancelToken = new CancelToken((cancel) =>
      handleRequestForPending(request, cancel)
    );
    return request;
  },
  (error) => {
    Promise.reject(error);
  }
);

送信中リストを作成し、リクエストに関する情報を文字列化し、送信中リストに追加します。 もし、送信中リストに、同じリクエストが存在する場合は、二重リクエストが発生しているので、キャンセルします。

リクエスト情報は、URL、HTTPメソッド、パラメータ、bodyデータを利用しています。

import { AxiosRequestConfig, Canceler } from "axios";

// 送信中リスト
let pending: Record<string, null> = {};

export const handleRequestForPending = (
  request: AxiosRequestConfig,
  cancel: Canceler
) => {
  const url = request.url;

  // 重複チェックに使うflagUrlを生成 (url, method, params, data)
  const flagUrl = `${url}&${request.method}&${JSON.stringify(
    request.params
  )}&${JSON.stringify(request.data)}`;

  // 送信中リストにあるリクエストと同じリクエストならキャンセル
  if (flagUrl in pending) {
    cancel(JSON.stringify(request));
  } else {
    // 送信中リストに追加
    pending[flagUrl] = null;
  }
};

レスポンスが返ってきたら、送信中リストから消す

レスポンスが返ってきたら、送信中リストから該当のリクエストを削除します。 また、もしリクエストがキャンセルされた場合にログを出力するようにしています。

// レスポンス用
axiosInstance.interceptors.response.use(
  (response) => {
    // 送信中リストから該当のrequestを消す
    handleResponseForPending(response.config);
    return response;
  },
  (error) => {
    if (axios.isCancel(error)) {
      // キャンセルされた旨をログに出力
      const canceledRequest = JSON.parse(error.message);
      console.error(
        `Canceled - ${canceledRequest.method.toUpperCase()}: ${
          canceledRequest.url
        } (params: ${JSON.stringify(canceledRequest.data)})`
      );
      return Promise.reject(error);
    }

    return Promise.reject(error);
  }
);

リクエストのハンドリング時は、request.dataがオブジェクトだったので、JSON.stringifyしていました。 しかし、レスポンスのハンドリング時のrequest.dataは文字列なので注意。

export const handleResponseForPending = (request: AxiosRequestConfig) => {
  const url = request.url;

  // 重複チェックに使うflagUrlを生成 (url, method, params, data)
  const flagUrl = `${url}&${request.method}&${JSON.stringify(request.params)}&${
    request.data
  }`;

  // 送信中リストから、該当のリクエストを消す
  if (flagUrl in pending) {
    delete pending[flagUrl];
  }
};

後処理

意図しないことが起こり、送信中リストにリクエストが残り続けてしまうのを防ぐために、SPAのページ遷移時にリストをリセットするようにしました。

export const resetPending = () => {
  pending = {};
};

Next.jsだと、以下のように実装する

import Router from "next/router";
Router.events.on("routeChangeStart", () => {
  // ページ遷移時にリクエストのペンディングリストをリセットする
  resetPending();
});