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 because the session has expired, after calling a refresh webservice that revives the session.
The problem is that the "done" callbacks are not called on a successful retry, unlike the "success" ajax option callback that is called. I've made up a simple example below:
$.ajaxSetup({statusCode: { 404: function() { this.url = '/existent_url'; $.ajax(this); } }}); $.ajax({ url: '/inexistent_url', success: function() { alert('success'); } }) .done(function() { alert('done'); });
Is there a way to have done-style callbacks called on a successful retry? I know a deferred can't be 'resolved' after it was 'rejected', is it possible to prevent the reject? Or maybe copy the doneList of the original deferred to a new deferred? I'm out of ideas:)
A more realistic example below, where I'm trying to queue up all 401-rejected requests, and retry them after a successful call to /refresh.
var refreshRequest = null, waitingRequests = null; var expiredTokenHandler = function(xhr, textStatus, errorThrown) { //only the first rejected request will fire up the /refresh call if(!refreshRequest) { waitingRequests = $.Deferred(); refreshRequest = $.ajax({ url: '/refresh', success: function(data) { // session refreshed, good refreshRequest = null; waitingRequests.resolve(); }, error: function(data) { // session can't be saved waitingRequests.reject(); alert('Your session has expired. Sorry.'); } }); } // put the current request into the waiting queue (function(request) { waitingRequests.done(function() { // retry the request $.ajax(request); }); })(this); } $.ajaxSetup({statusCode: { 401: expiredTokenHandler }});
The mechanism works, the 401-failed requests get fired a second time, the problem is their 'done' callbacks do not get called, so the applications stalls.
Deferred() method in JQuery is a function which returns the utility object with methods which can register multiple callbacks to queues. It calls the callback queues, and relay the success or failure state of any synchronous or asynchronous function.
ajax returns, which is a jqXHR object that conforms to the promise interface. If there is a failure, the outer fail function is invoked. The outer fail function is also invoked if the processData function fails. When both the getData and processData functions are successful, the outer done method is invoked.
Definition and Usage. The ajaxSetup() method sets default values for future AJAX requests.
ajax() is asynchronous, so immediately after executing the statement, the outer function returns, so there is no return value of the outer function, that's why you're getting undefined .
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.
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); });
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With