Angelin
Angelin

Reputation: 648

AngularJS service retry when promise is rejected

I'm getting data from an async service inside my controller like this:

myApp.controller('myController', ['$scope', 'AsyncService',
function($scope, AsyncService) {
    $scope.getData = function(query) {
        return AsyncService.query(query).then(function(response) {
            // Got success response, return promise
            return response;
        }, function(reason) {
            // Got error, query again in one second
            // ???
        });
    }
}]);

My questions:

  1. How to query the service again when I get error from service without returning the promise.
  2. Would it be better to do this in my service?

Thanks!

Upvotes: 16

Views: 23591

Answers (5)

Ratan Kumar
Ratan Kumar

Reputation: 1650

Following this article Promises in AngularJS, Explained as a Cartoon

you need to retry only when the response comes under 5XX category

I have written a service called http which can be called by passing all http configs as

 var params = {
  method: 'GET',
  url: URL,
  data: data
 }

then call the service method as follows:

   <yourDefinedAngularFactory>.http(params, function(err, response) {});

http: function(config, callback) {
  function request() {
    var counter = 0;
    var queryResults = $q.defer();

    function doQuery(config) {
      $http(config).success(function(response) {
        queryResults.resolve(response);
      }).error(function(response) {
        if (response && response.status >= 500 && counter < 3) {
          counter++;
          console.log('retrying .....' + counter);
          setTimeout(function() {
            doQuery(config);
          }, 3000 * counter);
        } else {
          queryResults.reject(response);
        }
      });
    }
    doQuery(config);
    return queryResults.promise;
  }
  request(config).then(function(response) {
    if (response) {
      callback(response.errors, response.data);
    } else {
      callback({}, {});
    }
  }, function(response) {
    if (response) {
      callback(response.errors, response.data);
    } else {
      callback({}, {});
    }
  });
}

Upvotes: 2

beckyconning
beckyconning

Reputation: 11

I ended up doing this a lot so I wrote a library to help address this problem : )

https://www.npmjs.com/package/reattempt-promise-function

In this example you could do something like

myApp.controller('myController', ['$scope', 'AsyncService',
function($scope, AsyncService) {
    var dogsQuery = { family: canine };
    $scope.online = true;
    $scope.getDogs = function() {
        return reattempt(AsyncService.query(dogsQuery)).then(function(dogs) {
            $scope.online = true;
            $scope.dogs = dogs;
        }).catch(function() {
            $scope.online = false;
        });
    }
}]);

Upvotes: 1

mathewguest
mathewguest

Reputation: 640

I wrote an implementation with exponential backoff that doesn't use recursion (which would created nested stack frames, correct?) The way it's implemented has the cost of using multiple timers and it always creates all the stack frames for the make_single_xhr_call (even after success, instead of only after failure). I'm not sure if it's worth it (especially if the average case is a success) but it's food for thought.

I was worried about a race condition between calls but if javascript is single-threaded and has no context switches (which would allow one $http.success to be interrupted by another and allow it to execute twice), then we're good here, correct?

Also, I'm very new to angularjs and modern javascript so the conventions may be a little dirty also. Let me know what you think.

var app = angular.module("angular", []);

app.controller("Controller", ["$scope", "$http", "$timeout",
    function($scope, $http, $timeout) {

  /**
   * Tries to make XmlHttpRequest call a few times with exponential backoff.
   * 
   * The way this works is by setting a timeout for all the possible calls
   * to make_single_xhr_call instantly (because $http is asynchronous) and
   * make_single_xhr_call checks the global state ($scope.xhr_completed) to
   * make sure another request was not already successful.
   *
   * With sleeptime = 0, inc = 1000, the calls will be performed around:
   * t = 0
   * t = 1000 (+1 second)
   * t = 3000 (+2 seconds)
   * t = 7000 (+4 seconds)
   * t = 15000 (+8 seconds)
   */
  $scope.repeatedly_xhr_call_until_success = function() {
    var url = "/url/to/data";
    $scope.xhr_completed = false
    var sleeptime = 0;
    var inc = 1000;
    for (var i = 0, n = 5 ; i < n ; ++i) {
      $timeout(function() {$scope.make_single_xhr_call(url);}, sleeptime);
      sleeptime += inc;
      inc = (inc << 1); // multiply inc by 2
    }
  };

  /**
   * Try to make a single XmlHttpRequest and do something with the data.
   */
  $scope.make_single_xhr_call = function(url) {
    console.log("Making XHR Request to " + url);

    // avoid making the call if it has already been successful
    if ($scope.xhr_completed) return;
    $http.get(url)
      .success(function(data, status, headers) {
        // this would be later (after the server responded)-- maybe another
        // one of the calls has already completed.
        if ($scope.xhr_completed) return;
        $scope.xhr_completed = true;
        console.log("XHR was successful");
        // do something with XHR data
      })
      .error(function(data, status, headers) {
        console.log("XHR failed.");
      });
  };

}]);

Upvotes: 2

Dorian
Dorian

Reputation: 23949

This actually works:

angular.module('retry_request', ['ng'])
  .factory('RetryRequest', ['$http', '$q', function($http, $q) {
    return function(path) {
      var MAX_REQUESTS = 3,
          counter = 1,
          results = $q.defer();

      var request = function() {
        $http({method: 'GET', url: path})
          .success(function(response) {
            results.resolve(response)
          })
          .error(function() {
            if (counter < MAX_REQUESTS) {
              request();
              counter++;
            } else {
              results.reject("Could not load after multiple tries");
            }
          });
      };

      request();

      return results.promise;
    }
  }]);

Then just an example of using it:

RetryRequest('/api/token').then(function(token) {
  // ... do something
});

You have to require it when declaring your module:

angular.module('App', ['retry_request']);

And in you controller:

app.controller('Controller', function($scope, RetryRequest) {
  ...
});

If someone wants to improve it with some kind of backoff or random timing to retry the request, that will be even better. I wish one day something like that will be in Angular Core

Upvotes: 9

M.K. Safi
M.K. Safi

Reputation: 7019

You can retry the request in the service itself, not the controller.

So, AsyncService.query can be something like:

AsyncService.query = function() {
  var counter = 0
  var queryResults = $q.defer()

  function doQuery() {
    $http({method: 'GET', url: 'https://example.com'})
      .success(function(body) {
        queryResults.resolve(body)
      })
      .error(function() {
        if (counter < 3) {
          doQuery()
          counter++ 
        }
      })
  }

  return queryResults.promise
}

And you can get rid of your error function in the controller:

myApp.controller('myController', ['$scope', 'AsyncService',
  function($scope, AsyncService) {
    $scope.getData = function(query) {
      return AsyncService.query(query).then(function(response) {
        // Got success response
        return response;
      });
    }
  }
]);

Upvotes: 22

Related Questions