web-dev-qa-db-fra.com

Réessayer une requête jQuery ajax qui a des rappels attachés à sa requête

J'essaie de mettre en œuvre un système de réessayer les demandes ajax qui échouent pour une raison temporaire. Dans mon cas, il s'agit de réessayer les demandes qui ont échoué avec un code d'état 401 parce que la session a expiré, après avoir appelé un service Web d'actualisation qui relance la session.

Le problème est que les rappels "done" ne sont pas appelés en cas de nouvelle tentative, contrairement au rappel d'option ajax "success" appelé. J'ai créé 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');
});

Existe-t-il un moyen de faire en sorte que les rappels de type terminé soient appelés lors d'une nouvelle tentative? 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 liste done de l'original différé dans un nouveau différé? Je suis à court d'idées :)

Un exemple plus réaliste ci-dessous, dans lequel j'essaie de mettre en file d'attente toutes les demandes rejetées par 401, et de les réessayer après un appel réussi à/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
}});

Le mécanisme fonctionne, les demandes ayant échoué 401 sont renvoyées une seconde fois. Le problème est que leurs rappels "effectués" ne sont pas appelés, de sorte que les applications se bloquent.

36
cipak

Comme le note la réponse de gnarf, les rappels de succès et d’erreur ne se comporteront pas comme prévu. Si vous êtes intéressé, voici une version qui prend en charge les rappels success et error ainsi que les événements de style promesses.

$.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

J'ai créé un plugin jQuery pour ce cas d'utilisation. Il encapsule la logique décrite dans la réponse de gnarf dans un plugin et vous permet également de spécifier un délai d'attente avant de tenter à nouveau l'appel ajax. Par 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
johnkpaul

Est-ce que quelque chose comme ça marcherait pour vous? Il vous suffit de renvoyer votre propre promesse différée/différée pour que la première ne soit pas rejetée trop tôt.

Exemple/test d'utilisation: 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

C’est une grande question à laquelle je viens tout juste de faire face.

La réponse acceptée (de @gnarf) m'a décontenancée. J'ai donc trouvé un moyen plus facile à comprendre:

        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, je viens de résumer l'appel Ajax entier et ses rappels dans une fonction qui peut être appelée de manière récursive.

0
Ryan