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

风格不统一 提交于 2019-11-28 03:40:43

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

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!");
});  

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

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!