Search code examples
javascriptajaxxmlhttprequestpromiseq

How to retry an xhr request which returns a promise recursively for atleast n times on status 0


I have written the below piece of code. makeRequest gets called and I want to retry this when xhr status is 0. The problem is I am not able to resolve the correct promise, the retry logic fetches the correct response in nth attempt but fails to propagate to the caller method.

How do I resolve this issue.

var makeRequest = function(method, urlToBeCalled, payload) {
  var deferred = $q.defer();
  var xhr = new XMLHttpRequest();
  xhr.open(method, encodeURI(urlToBeCalled), true);
  setHttpRequestHeaders(xhr); // set headers
  var response;
  xhr.onload = function() {
    if (xhr.status === 200 && xhr.readyState === 4 && xhr.getResponseHeader('content-type') !==
      'text/html') {
      try {
        response = JSON.parse(xhr.response);
        deferred.resolve(response);
      } catch (e) {
        deferred.reject(e);
      }
    } else if (xhr.status === 0) {
      // retry here;
      deferred.resolve(makeRequest(method, urlToBeCalled, payload));
    } else {
      try {
        response = JSON.parse(xhr.response);
        deferred.reject(response);
      } catch (e) {
        deferred.reject(xhr.response);
      }
    }
  };
  xhr.onerror = function() {
    deferred.reject(xhr.response);
  };
  xhr.send(payload);
  return deferred.promise;
};

Solution

  • Here's how I'd approach it (see *** comments):

    var makeRequest = function(method, urlToBeCalled, payload) {
        var deferred = $q.defer();
        var retries = 4;                     // *** Counter
        run();                               // *** Call the worker
        return deferred.promise;
    
        // *** Move the actual work to its own function
        function run() {
            var xhr = new XMLHttpRequest();
            xhr.open(method, encodeURI(urlToBeCalled), true);
            setHttpRequestHeaders(xhr);
            xhr.onload = function() {
                if (xhr.status === 200 && xhr.readyState === 4 && xhr.getResponseHeader('content-type') !== 'text/html') {
                    try {
                        response = JSON.parse(xhr.response);
                        deferred.resolve(response);
                    } catch (e) {
                        deferred.reject(e);
                    }
                } else if (xhr.status === 0) {
                    // retry
                    if (retries--) {          // *** Recurse if we still have retries 
                        run();
                    } else {
                        // *** Out of retries
                        deferred.reject(e);
                    }
                } else {
                    // *** See note below, probably remove this
                    try {
                        response = JSON.parse(xhr.response);
                        deferred.reject(response);
                    } catch (e) {
                        deferred.reject(xhr.response);
                    }
                }
            };
            xhr.onerror = function() {
                deferred.reject(xhr.response);
            };
            xhr.send(payload);
        }
    };
    

    Side note: The content of your initial if body and the final else appear to be identical. I think I'd recast the entire onload:

    xhr.onload = function() {
        if (xhr.readyState === 4) {
            // It's done, what happened?
            if (xhr.status === 200) {
                if (xhr.getResponseHeader('content-type') !== 'text/html') {
                    try {
                        response = JSON.parse(xhr.response);
                        deferred.resolve(response);
                    } catch (e) {
                        deferred.reject(e);
                    }
                } else {
                    // Something went wrong?
                    deferred.reject(e);
                }
            } else if (xhr.status === 0) {
                // retry
                if (retries--) {          // *** Recurse if we still have retries 
                    run();
                } else {
                    // *** Out of retries
                    deferred.reject(e);
                }
            }
        }
    };
    

    Re your comment:

    This does resolve my current problem but is there a way to resolve all the promises which are added to call stack if any one of those is resolved?

    Yes: To do that with Angular's $q (I assume that's what you're using), you can just pass the promise you get back from the recursive call into resolve on your deferred object: Since it's a promise, the deferred will wait for it to be settled and resolve or reject based on what that promise does. If you do this at every level in the chain, the resolutions work their way up the chain:

    angular.module("mainModule", []).controller(
      "mainController",
      function($scope, $q, $http) {
        test(true).then(function() {
          test(false);
        });
    
        function test(flag) {
          log(flag ? "Testing resolved" : "Testing rejected");
          return recursive(3, flag)
            .then(function(arg) {
              log("Resolved with", arg);
            })
            .catch(function(arg) {
              log("Rejected with", arg);
            });
        }
    
        function recursive(count, flag) {
          log("recursive(" + count + ", " + flag + ") called");
          var d = $q.defer();
          setTimeout(function() {
            if (count <= 0) {
              // Done, settle
              if (flag) {
                log("Done, resolving with " + count);
                d.resolve(count);
              } else {
                log("Done, rejecting with " + count);
                d.reject(count);
              }
            } else {
              // Not done, resolve with promise from recursive call
              log("Not done yet, recursing with " + (count - 1));
              d.resolve(recursive(count - 1, flag));
            }
          }, 0);
          return d.promise;
        }
      }
    );
    
    function log() {
      var p = document.createElement('pre');
      p.appendChild(
        document.createTextNode(
          Array.prototype.join.call(arguments, " ")
        )
      );
      document.body.appendChild(p);
    }
    pre {
      margin: 0;
      padding: 0;
    }
    <div ng-app="mainModule">
      <div ng-controller="mainController"></div>
    </div>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

    You can do the same thing with JavaScript's own promises:

    test(true).then(function() {
      test(false);
    });
    
    function test(flag) {
      log(flag ? "Testing resolved" : "Testing rejected");
      return recursive(3, flag)
        .then(function(arg) {
          log("Resolved with", arg);
        })
        .catch(function(arg) {
          log("Rejected with", arg);
        });
    }
    
    function recursive(count, flag) {
      log("recursive(" + count + ", " + flag + ") called");
      return new Promise(function(resolve, reject) {
        setTimeout(function() {
          if (count <= 0) {
            // Done, resolve with value
            if (flag) {
              log("Done, resolving with " + count);
              resolve(count);
            } else {
              log("Done, rejecting with " + count);
              reject(count);
            }
          } else {
            // Not done, resolve with promise
            // from recursive call
            log("Not done yet, recursing with " + (count - 1));
            resolve(recursive(count - 1, flag));
          }
        }, 0);
      });
    }
    
    function log() {
      var p = document.createElement('pre');
      p.appendChild(
        document.createTextNode(
          Array.prototype.join.call(arguments, " ")
        )
      );
      document.body.appendChild(p);
    }
    pre {
      margin: 0;
      padding: 0;
    }