arbghl arbghl - 4 months ago 42
Ajax Question

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;
};

Answer

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;
}

Comments