web-dev-qa-db-ja.com

遅延にアタッチされたコールバックを持つjquery ajaxリクエストを再試行します

一時的な理由で失敗したajaxリクエストを再試行するシステムを実装しようとしています。私の場合、それは、セッションを復活させる更新Webサービスを呼び出した後、セッションの有効期限が切れたために401ステータスコードで失敗したリクエストを再試行することです。

問題は、呼び出された「成功」のajaxオプションコールバックとは異なり、成功した再試行では「完了」のコールバックが呼び出されないことです。以下の簡単な例を作りました:

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

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

再試行が成功したときに、完了したスタイルのコールバックを呼び出す方法はありますか?遅延オブジェクトが「拒否」された後に「解決」できないことを知っていますが、拒否を防ぐことはできますか?または、元の遅延オブジェクトのdoneListを新しい遅延オブジェクトにコピーしますか?私はアイデアがありません:)

以下のより現実的な例では、401で拒否されたすべての要求をキューに入れ、/ 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
}});

このメカニズムは機能し、401回失敗したリクエストは2回目に発生します。問題は、「完了した」コールバックが呼び出されないため、アプリケーションが停止することです。

37
cipak

_jQuery.ajaxPrefilter_ を使用して、jqXHRを別の deferred object でラップできます。

私は 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's promise functions with our deferred
    return dfd.promise(jqXHR);
});
_

deferred.promise(object) は実際にはjqXHRのすべての「promiseメソッド」を上書きするため、これは機能します。

注:ajaxオプションで_success:_および_error:_を使用してコールバックをアタッチしている場合、このスニペットは期待どおりに機能しません。コールバックは、jqXHRの.done(callback)および.fail(callback)メソッドを使用してアタッチされたコールバックのみであると想定しています。

51
gnarf

Gnarfの回答ノートとして、成功とエラーのコールバックは期待どおりに動作しません。誰もが興味を持っている場合、successerrorの両方のコールバックとpromiseスタイルのイベントをサポートするバージョンがあります。

$.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);
});
15
ryan

この使用例のために jQuery plugin を作成しました。これは、プラグインの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!");
});  
9
johnkpaul

このようなことがうまくいきますか?元のDeferred/Promiseがすぐに拒否されないように、元のDeferred/Promiseを返す必要があります。

使用例/テスト: 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();
}
6
dherman

これは私が直面した素晴らしい質問です。

受け入れられた回答(@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呼び出し全体を再帰的に呼び出すことができる1つの関数にラップしただけです。

0
Ryan