Повторите запрос ajax jquery, который имеет обратные вызовы, связанные с его отложенным

Я пытаюсь реализовать систему повторных попыток ajax-запросов, которые не срабатывают по временной причине. В моем случае речь идет о повторном запросе запросов с кодом состояния 401, поскольку срок действия сеанса истек, после вызова обновленного веб-сервиса, который оживляет сеанс.

Проблема заключается в том, что "сделанные" обратные вызовы не вызываются при успешной повторной попытке, в отличие от вызываемого обратного вызова опции "успех" ajax. Я составил простой пример ниже:

$.ajaxSetup({statusCode: {
    404: function() {
        this.url = '/existent_url';
        $.ajax(this);
    }
}});

$.ajax({
    url: '/inexistent_url',
    success: function() { alert('success'); }
})
.done(function() {
    alert('done');
});

Есть ли способ сделать обратные вызовы в стиле, вызванные успешной повторной попыткой? Я знаю, что отсроченный не может быть "разрешен" после того, как его "отвергли", можно ли предотвратить отказ? Или, может быть, скопировать сделанный список оригинала, отложенный до нового отложенного? У меня нет идей:)

Более реалистичный пример ниже, где я пытаюсь поставить в очередь все отклоненные запросы 401 и повторить их после успешного вызова/обновления.

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

Механизм работает, запросы с ошибкой 401 получают второй раз, проблема заключается в том, что их "сделанные" обратные вызовы не вызываются, поэтому приложения закрываются.

Ответ 1

Вы можете использовать jQuery.ajaxPrefilter для переноса jqXHR в другой отложенный объект.

Я сделал пример на jsFiddle, который показывает, что он работает, и попытался адаптировать часть вашего кода для обработки 401 в эту версию

$.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 promise functions with our deferred
    return dfd.promise(jqXHR);
});

Это работает, потому что deferred.promise(object) фактически перезапишет все "методы обещания" на jqXHR.

ПРИМЕЧАНИЕ.. Если кто-либо еще находит это, если вы добавляете обратные вызовы с success: и error: в параметрах ajax, этот фрагмент будет не работать так вы ожидаете. Он предполагает, что единственными обратными вызовами являются те, которые привязаны с использованием методов .done(callback) и .fail(callback) для jqXHR.

Ответ 2

Как примечания ответа gnarf, успешные и обратные вызовы ошибок не будут вести себя так, как ожидалось. Если кому-то интересно, это версия, которая поддерживает обратные вызовы success и error, а также события стиля promises.

$.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 promise functions with our deferred
    return dfd.promise(jqXHR);
});

Ответ 3

Я создал плагин jQuery для этого варианта использования. Он завершает логику, описанную в ответе gnarf в плагине, и дополнительно позволяет указать тайм-аут, чтобы ждать, прежде чем повторять попытку вызова ajax. Например.

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

Ответ 4

Будет ли что-то вроде этого работать для вас? Вам просто нужно вернуть свою собственную Отсрочку/Обещать, чтобы исходный не был отклонён слишком рано.

Пример/использование теста: 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();
}

Ответ 5

Это отличный вопрос, с которым я тоже столкнулся.

Я был обескуражен принятым ответом (от @gnarf), поэтому я понял способ, которым я понял легче:

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

В принципе, я просто завернул весь вызов Ajax и его обратные вызовы в одну функцию, которая может быть вызвана рекурсивно.