I\'m trying to implement a system of retrying ajax requests that fail for a temporary reason. In my case, it is about retrying requests that failed with a 401 status code be
This is a great question that I just faced too.
I was daunted by the accepted answer (from @gnarf), so I figured out a way that I understood easier:
var retryLimit = 3;
var tryCount = 0;
callAjax(payload);
function callAjax(payload) {
tryCount++;
var newSaveRequest = $.ajax({
url: '/survey/save',
type: 'POST',
data: payload,
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
error: function (xhr, textStatus, errorThrown) {
if (textStatus !== 'abort') {
console.log('Error on ' + thisAnswerRequestNum, xhr, textStatus, errorThrown);
if (tryCount <= retryLimit) {
sleep(2000).then(function () {
if ($.inArray(thisAnswerRequestNum, abortedRequestIds) === -1) {
console.log('Trying again ' + thisAnswerRequestNum);
callAjax(payload);//try again
}
});
return;
}
return;
}
}
});
newSaveRequest.then(function (data) {
var newData = self.getDiffFromObjects(recentSurveyData, data);
console.log("Answer was recorded " + thisAnswerRequestNum, newData);//, data, JSON.stringify(data)
recentSurveyData = data;
});
self.previousQuizAnswerAjax = newSaveRequest;
self.previousQuizAnswerIter = thisAnswerRequestNum;
}
function sleep(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
Basically, I just wrapped the entire Ajax call and its callbacks into one function which can get called recursively.
As gnarf's answer notes, success and error callbacks will not behave as expected. If anyone is interested here is a version that supports both success
and error
callbacks as well as promises style events.
$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
// Don't infinitely recurse
originalOptions._retry = isNaN(originalOptions._retry)
? Common.auth.maxExpiredAuthorizationRetries
: originalOptions._retry - 1;
// set up to date authorization header with every request
jqXHR.setRequestHeader("Authorization", Common.auth.getAuthorizationHeader());
// save the original error callback for later
if (originalOptions.error)
originalOptions._error = originalOptions.error;
// overwrite *current request* error callback
options.error = $.noop();
// setup our own deferred object to also support promises that are only invoked
// once all of the retry attempts have been exhausted
var dfd = $.Deferred();
jqXHR.done(dfd.resolve);
// if the request fails, do something else yet still resolve
jqXHR.fail(function () {
var args = Array.prototype.slice.call(arguments);
if (jqXHR.status === 401 && originalOptions._retry > 0) {
// refresh the oauth credentials for the next attempt(s)
// (will be stored and returned by Common.auth.getAuthorizationHeader())
Common.auth.handleUnauthorized();
// retry with our modified
$.ajax(originalOptions).then(dfd.resolve, dfd.reject);
} else {
// add our _error callback to our promise object
if (originalOptions._error)
dfd.fail(originalOptions._error);
dfd.rejectWith(jqXHR, args);
}
});
// NOW override the jqXHR's promise functions with our deferred
return dfd.promise(jqXHR);
});
You could use jQuery.ajaxPrefilter to wrap the jqXHR in another deferred object.
I made an example on jsFiddle that shows it working, and tried to adapt some of your code to handle the 401 into this version:
$.ajaxPrefilter(function(opts, originalOpts, jqXHR) {
// you could pass this option in on a "retry" so that it doesn't
// get all recursive on you.
if (opts.refreshRequest) {
return;
}
// our own deferred object to handle done/fail callbacks
var dfd = $.Deferred();
// if the request works, return normally
jqXHR.done(dfd.resolve);
// if the request fails, do something else
// yet still resolve
jqXHR.fail(function() {
var args = Array.prototype.slice.call(arguments);
if (jqXHR.status === 401) {
$.ajax({
url: '/refresh',
refreshRequest: true,
error: function() {
// session can't be saved
alert('Your session has expired. Sorry.');
// reject with the original 401 data
dfd.rejectWith(jqXHR, args);
},
success: function() {
// retry with a copied originalOpts with refreshRequest.
var newOpts = $.extend({}, originalOpts, {
refreshRequest: true
});
// pass this one on to our deferred pass or fail.
$.ajax(newOpts).then(dfd.resolve, dfd.reject);
}
});
} else {
dfd.rejectWith(jqXHR, args);
}
});
// NOW override the jqXHR's promise functions with our deferred
return dfd.promise(jqXHR);
});
This works because deferred.promise(object) will actually overwrite all of the "promise methods" on the jqXHR.
NOTE: To anyone else finding this, if you are attaching callbacks with success:
and error:
in the ajax options, this snippet will not work the way you expect. It assumes that the only callbacks are the ones attached using the .done(callback)
and .fail(callback)
methods of the jqXHR.
Would something like this work out for you? You just need to return your own Deferred/Promise so that the original one isn't rejected too soon.
Example/test usage: http://jsfiddle.net/4LT2a/3/
function doSomething() {
var dfr = $.Deferred();
(function makeRequest() {
$.ajax({
url: "someurl",
dataType: "json",
success: dfr.resolve,
error: function( jqXHR ) {
if ( jqXHR.status === 401 ) {
return makeRequest( this );
}
dfr.rejectWith.apply( this, arguments );
}
});
}());
return dfr.promise();
}
I have created a jQuery plugin for this use case. It wraps the logic described in gnarf's answer in a plugin and additionally allows you to specify a timeout to wait before attempting the ajax call again. For example.
//this will try the ajax call three times in total
//if there is no error, the success callbacks will be fired immediately
//if there is an error after three attempts, the error callback will be called
$.ajax(options).retry({times:3}).then(function(){
alert("success!");
});
//this has the same sematics as above, except will
//wait 3 seconds between attempts
$.ajax(options).retry({times:3, timeout:3000}).retry(3).then(function(){
alert("success!");
});