Réessayer Une demande ajax de jquery qui a des callbacks attachés à son différé

j'essaie de mettre en place un système de rétraction des requêtes ajax qui échouent pour une raison temporaire. Dans mon cas, il s'agit de retracer des requêtes qui ont échoué avec un code de statut 401 parce que la session a expiré, après avoir appelé un service Web de rafraîchissement qui relance la session.

le problème est que les callbacks" done "ne sont pas appelés sur un ré-essai réussi, contrairement à l'option" success " ajax callback qui est appelé. J'ai fait un exemple simple ci-dessous:

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

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

y a-t-il une façon d'avoir fait-callbacks style appelé sur un essai réussi? Je sais qu'un différé ne peut pas être "résolu" après avoir été "rejeté", est-il possible d'empêcher le rejet? Ou peut-être Copier la doneliste de l'original différé à un nouveau différé? Je suis à court d'idées:)

un exemple plus réaliste ci-dessous, où j'essaie de faire la queue pour toutes les requêtes 401-rejetées, et de les réessayer après un appel réussi à /rafraîchir.

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

le mécanisme fonctionne, les requêtes 401-failed sont lancées une deuxième fois, le problème est que leurs callbacks 'done' ne sont pas appelés, donc les applications décrochent.

36
demandé sur BartoszKP 2012-08-03 13:56:55

5 réponses

vous pouvez utiliser jQuery.ajaxPrefilter pour envelopper le jqXHR dans un autre objet différé .

j'ai fait un exemple sur jsFiddle qui montre qu'il fonctionne, et j'ai essayé d'adapter une partie de votre code pour gérer la 401 dans cette 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);
});

cela fonctionne parce que deferred.promise(object) va en fait écraser toutes les" méthodes promises " sur le jqXHR.

NOTE: à toute autre personne trouvant cela, si vous attachez des callbacks avec success: et error: dans les options ajax, cet extrait fonctionnera pas comme vous l'attendez. Il suppose que les seules callbacks sont ceux attachés en utilisant les méthodes .done(callback) et .fail(callback) du jqXHR.

48
répondu gnarf 2012-10-09 19:18:47

comme le note la réponse de gnarf, les callbacks de succès et d'erreurs ne se comporteront pas comme prévu. Si quelqu'un est intéressé, voici une version qui supporte les callbacks success et error ainsi que les évènements de style 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's promise functions with our deferred
    return dfd.promise(jqXHR);
});
15
répondu ryan 2012-10-11 13:25:40

j'ai créé un plugin jQuery pour ce cas d'utilisation. Il enroule la logique décrite dans la réponse de gnarf dans un plugin et vous permet en outre de spécifier un délai d'attente avant d'essayer l'appel ajax à nouveau. Exemple.

//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
répondu johnkpaul 2012-09-22 17:51:12

est-ce que quelque chose comme ça pourrait marcher pour vous? Vous avez juste besoin de retourner votre propre différé/promesse afin que l'original n'est pas rejeté trop tôt.

exemple d'utilisation/essai: 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
répondu dherman 2012-09-13 03:24:05

C'est une grande question à laquelle je viens de faire face aussi.

j'ai été hanté par la réponse acceptée (de @gnarf), donc j'ai trouvé un moyen que j'ai compris plus facile:

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

fondamentalement, j'ai juste enveloppé L'appel Ajax entier et ses callbacks dans une fonction qui peut être appelé récursivement.

0
répondu Ryan 2017-03-01 21:16:15