AngularJS service retry when promise is rejected
Asked Answered
B

5

16

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!

Bergmans answered 25/10, 2013 at 17:20 Comment(3)
Yes, re-fire in the service, that way in the controller you can simply have the resolved data.Krafftebing
define your function and name it something. In rejection call it. That simple!Javelin
tried to return $scope.getData(query) in controller but the promise is no longer sentBergmans
A
22

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;
      });
    }
  }
]);
Allsopp answered 25/10, 2013 at 17:42 Comment(8)
May want to have a counter, if the server is down this will just loop over and over.Krafftebing
Or maybe it should be a backoff, with a x2 delay after every failure?Flem
Is there any way to make this into a universal http wrapper type scenario. To automatically do this kind of check on every http call?Blabber
@AugieGardner Perhaps by using application-level $http request interceptors in Angular.Allsopp
When does doQuery() get called the first time?Hobby
@arkanciscan Yeah, it's an error with the code. You have to call doQuery() from within the AsyncService function.Allsopp
Some notes on implementing exponential backoff (the 2x delay after every failure): developers.google.com/analytics/devguides/reporting/core/v3/…Peregrination
This code will not work un Angular 1.4+. As the documentation says: "The $http legacy promise methods success and error have been deprecated. Use the standard then method instead...."Baptism
F
9

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

Flem answered 4/2, 2014 at 22:46 Comment(3)
In .error, it would probably be good to add: else { results.reject(path + ' failed after ' + MAX_REQUESTS + ' retries.'); }Cremona
Maybe do results.reject(...) to properly handle the errorFlem
So I tried it and it works fine (reject after 3 errors)Flem
R
2

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.");
      });
  };

}]);
Ruin answered 18/11, 2014 at 21:35 Comment(0)
P
2

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({}, {});
    }
  });
}
Pornography answered 25/11, 2015 at 12:2 Comment(1)
Good pick for the 5XX errors. It may be worthy to also retry when error.status equals -1 (timeout, no connection, ...).Indiscrimination
R
1

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;
        });
    }
}]);
Renny answered 5/1, 2015 at 1:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.