Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Retry a jquery ajax request which has callbacks attached to its deferred

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.

like image 721
cipak Avatar asked Aug 03 '12 09:08

cipak


People also ask

When JQuery deferred?

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.

Does an ajax request return a promise?

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.

What is ajaxSetup?

Definition and Usage. The ajaxSetup() method sets default values for future AJAX requests.

Why is ajax returning undefined?

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 .


2 Answers

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.

like image 98
gnarf Avatar answered Sep 25 '22 04:09

gnarf


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); }); 
like image 23
ryan Avatar answered Sep 25 '22 04:09

ryan