AngularJS で二重ポストしないようにする

これまで Ajax で二重ポストをしないようにするには、 DOM を操作して disable にして押せなくしたり、できないものは HTTP リクエストする前に変数に状態を持たせてレスポンスがきたらその状態をクリアしたりするといったものがありましたが、リクエストを出す処理ごとに二重ポストを防止するコードを入れなければいけないことが多く面倒でした。

もう少しスマートなやり方ないかな、と調べていたところ丁度使用している AngularJS でよい感じに解決しているブログを見つけましたので紹介します。

http://blog.codebrag.com/post/57412530001/preventing-duplicated-requests-in-angularjs

AngularJS は HTTP リクエスト後に結果が返ってきてないものを $http.pendingRequests に登録しています。二重ポストさせたくないリクエストにユニーク ID を HTTP リクエストに AngularJS へのオプションとして渡し、 pendingRequests の中身と比較して重複したものがあればリクエストを破棄するという AngularJS の仕組みを利用した面白い仕組みです。

こちらのアイデアをインターセプターにしてモジュール化したものを書いてみました。 AngularJS 1.2.13 で動作を確認しています。 (Array#filter を使ってるので古い IE をサポートする場合は変えてください)

angular.module("rejectDuplicatedRequest", []).config(["$provide", "$httpProvider", function($provide, $httpProvider) {
  var interceptor = ["$q", "$injector", function ($q, $injector) {
    return {
      request: function(config) {
        var $http = $injector.get("$http");
        var uniqueRequestOptionName = "unique";
        var requestIdOptionName = "requestId";

        function checkForDuplicates(config) {
          return !!config[uniqueRequestOptionName];
        }

        function checkIfDuplicated(config) {
          var duplicated = $http.pendingRequests.filter(function(pendingConfig) {
            return pendingConfig[requestIdOptionName] && pendingConfig[requestIdOptionName] === config[requestIdOptionName];
          });
          return duplicated.length > 0;
        }

        function buildRejectedRequestPromise(config) {
          var dfd = $q.defer();
          // var response = {data: {}, headers: {}, status: 499, config: config};
          // dfd.reject(response);

          return dfd.promise;
        }

        if (checkForDuplicates(config) && checkIfDuplicated(config)) {
          return buildRejectedRequestPromise(config);
        }

        return config || $q.when(config);
      }
    };
  }];

  $httpProvider.interceptors.push(interceptor);
});

このモジュールを読み込んだ後に AngularJS の HTTP リクエストに以下のようなオプションを渡せば、同じリクエストは連続して行えなくなります。従来のやり方に比べ利用側はリクエストの状態管理をしなくてすみ、ロジックが二重ポストのために汚染されないところが良い点です。

$http.post(url, data, {
  unique: true,
  requestId: "uniqueId"
}).success(function(response) {
  // process
}).error(function(response, status) {
  // error handling
});

先に紹介したブログでは $http から直接呼んだ場合しか動かないと書いていますが、こちらはインターセプターを直接登録しているため get/post のショートカットメソッドから呼び出しても動くようになっています。

それから、この例では二重ポストが実行された場合に無視するようにしていますが、 error メソッドのコールバックで何かしらハンドリングさせたい場合はコメントアウトしている reject を実行することで可能です。このときにステータスコード 499 を返していますが、標準化されてるものでもないので別なものにしてよいかもしれません。ただ 400 を返すだけでは通常のリクエストエラーで 400 を返すような場合に判別がつかないので、何かしら工夫が必要になりそうです。

あと $injector.get("$http")でわざわざ $http を取り出しているのが、ちょっとイケてないですが $httpProvider と一緒に利用しようとすると循環参照になってしまいエラーとなってしまうため止む得ずそうしてます。

Uncaught Error: [$injector:cdep] Circular dependency found: $http <- $compile

AngularJS の機能を利用して二重ポストをちょっとスマートに回避する方法を紹介してみました。