40

一時的な理由で失敗した 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');
});

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

以下のより現実的な例では、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 度目に起動されます。問題は、「完了」コールバックが呼び出されないため、アプリケーションが停止することです。

4

5 に答える 5

54

jQuery.ajaxPrefilterjqXHR を別の遅延オブジェクトにラップするために使用できます。

私はそれが機能していることを示す例を作成し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:期待どおりに機能しません。唯一のコールバックは、jqXHRのおよびメソッドを使用してアタッチされたものであると想定しています。error:.done(callback).fail(callback)

于 2012-09-16T11:07:30.447 に答える
16

gnarfの回答が示すように、成功とエラーのコールバックは期待どおりに動作しません。誰かがここに興味があるなら、コールバックとプロミススタイルイベントの両方をサポートするバージョンがsuccessありerrorます。

$.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);
});
于 2012-10-11T13:25:40.417 に答える
9

このユースケース用に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!");
});  
于 2012-09-16T19:49:29.010 に答える
6

このようなことはうまくいきますか?元のものがすぐに拒否されないように、独自の 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();
}
于 2012-09-13T03:16:29.047 に答える