What's the best way to retry an AJAX request on failure using jQuery?
Asked Answered
M

9

127

Pseudo code:

$(document).ajaxError(function(e, xhr, options, error) {
  xhr.retry()
})

Even better would be some kind of exponential back-off

Marissamarist answered 5/4, 2012 at 7:38 Comment(3)
I'm not sure if this is the best way at all, so just a comment, but if you call your ajax from a fucntion, you can give it a parameter tries, and on fail you call your function with tries+1. Stop execution on tries==3 or any other number.Discussion
possible duplicate of Retry a jquery ajax request which has callbacks attached to its deferredConchoidal
Nice solution here: #6299112Gentilesse
H
274

Something like this:


$.ajax({
    url : 'someurl',
    type : 'POST',
    data :  ....,   
    tryCount : 0,
    retryLimit : 3,
    success : function(json) {
        //do something
    },
    error : function(xhr, textStatus, errorThrown ) {
        if (textStatus == 'timeout') {
            this.tryCount++;
            if (this.tryCount <= this.retryLimit) {
                //try again
                $.ajax(this);
                return;
            }            
            return;
        }
        if (xhr.status == 500) {
            //handle error
        } else {
            //handle error
        }
    }
});
Highland answered 5/4, 2012 at 7:44 Comment(14)
I've taken @Sudhir's solution and created a $.retryAjax plugin on github here: github.com/mberkom/jQuery.retryAjaxCommitteeman
This is not working for me. this.tryCount in the conditional is always 1.Udder
@MichaelBerkompas - does your plugin still work? It has not received commits in 2 years.Scopoline
@Scopoline It probably does, but I haven't been using it recently.Committeeman
will this work if another callback handler like .success is attached to calling the function that returns this ajax request?Behah
Pair of tryCount and retryLimit is excessive. Consider using only 1 variable: this.retryLimit--; if (this.retryLimit) { ... $.ajax(this) ... }Todhunter
If I want to retry the request a certain time later. For example, my server gives a 500 error sometimes when I load too many requests, and I want to try it again in say a minute with the same request, what would you recommend as the best way to do that from the structure above?Lazo
Great solution! I used something like that, to change URL for the next try: if(this.tryCount <= this.retryLimit) { // Try again with another URL $.ajax({ url: "some.url", dataType: this.dataType, success: this.success }); return; }Ribosome
Amazing! I have added additional parameter timeout : 3000 and then increased the timeout with every retry: this.timeout = this.timeout * this.tryCount; perhaps it might give more time for any of the requests to come back with a response.Caddish
What if you'd like to trigger ajax error inside success?Axolotl
@Udder - Try defining tryCount before $.ajax() var tryCount = 0 and change every instance of this.tryCount to tryCountLorenzalorenzana
Wouldn't this refer to the error callback function rather than the object passed to $.ajax()?Asbury
retryLimit : 3, // 重試次數 retryInterval : 2000, // 重試間隔毫秒數 error : function(xhr, textStatus, errorThrown ) { if (textStatus == 'timeout' || textStatus == 'error' ) { if (this.retryLimit-- > 0) { setTimeout(() => { console.log('try again 開始重試' + this.retryLimit + ', url:' + this.url); $.ajax(this); }, this.retryInterval || 100); return; } return; }Archeozoic
When I try to add tryCount : 0, retryLimit : 3, , I got an error?Cutlip
R
20

One approach is to use a wrapper function:

(function runAjax(retries, delay){
  delay = delay || 1000;
  $.ajax({
    type        : 'GET',
    url         : '',
    dataType    : 'json',
    contentType : 'application/json'
  })
  .fail(function(){
    console.log(retries); // prrint retry count
    retries > 0 && setTimeout(function(){
        runAjax(--retries);
    },delay);
  })
})(3, 100);

Another approach would be to use a retries property on the $.ajax

// define ajax settings
var ajaxSettings = {
  type        : 'GET',
  url         : '',
  dataType    : 'json',
  contentType : 'application/json',
  retries     : 3  //                 <-----------------------
};

// run initial ajax
$.ajax(ajaxSettings).fail(onFail)

// on fail, retry by creating a new Ajax deferred
function onFail(){
  if( ajaxSettings.retries-- > 0 )
    setTimeout(function(){
        $.ajax(ajaxSettings).fail(onFail);
    }, 1000);
}

Another way (GIST) - override original $.ajax (better for DRY)

// enhance the original "$.ajax" with a retry mechanism 
$.ajax = (($oldAjax) => {
  // on fail, retry by creating a new Ajax deferred
  function check(a,b,c){
    var shouldRetry = b != 'success' && b != 'parsererror';
    if( shouldRetry && --this.retries > 0 )
      setTimeout(() => { $.ajax(this) }, this.retryInterval || 100);
  }

  return settings => $oldAjax(settings).always(check)
})($.ajax);



// now we can use the "retries" property if we need to retry on fail
$.ajax({
    type          : 'GET',
    url           : 'http://www.whatever123.gov',
    timeout       : 2000,
    retries       : 3,     //       <-------- Optional
    retryInterval : 2000   //       <-------- Optional
})
// Problem: "fail" will only be called once, and not for each retry
.fail(()=>{
  console.log('failed') 
});

A point to consider is making sure the $.ajax method wasn't already wrapped previously, in order to avoid the same code running twice.


You can copy-paste these snippets (as-is) to the console to test them

Realm answered 1/10, 2016 at 23:17 Comment(0)
B
7

I've had a lot of success with this code below (example: http://jsfiddle.net/uZSFK/)

$.ajaxSetup({
    timeout: 3000, 
    retryAfter:7000
});

function func( param ){
    $.ajax( 'http://www.example.com/' )
        .success( function() {
            console.log( 'Ajax request worked' );
        })
        .error(function() {
            console.log( 'Ajax request failed...' );
            setTimeout ( function(){ func( param ) }, $.ajaxSetup().retryAfter );
        });
}
Baucom answered 25/8, 2012 at 9:37 Comment(3)
The only change I would suggest is to replace 'func("'+param"'")' with function(){func(param)}. That way, you can directly pass the parameter along without converting it to a string and back, which can fail very easily!Periapt
Isn't this an endless loop? Given the question has a retryLimit and is obviously wanting to cater for the server never coming back... I think this really has to be in thereEdwinaedwine
jQuery.ajaxSetup() Description: Set default values for future Ajax requests. Its use is not recommended. api.jquery.com/jQuery.ajaxSetupJointworm
O
3

Your code is almost full :)

const counter = 0;
$(document).ajaxSuccess(function ( event, xhr, settings ) {
    counter = 0;
}).ajaxError(function ( event, jqxhr, settings, thrownError ) {
    if (counter === 0 /*any thing else you want to check ie && jqxhr.status === 401*/) {
        ++counter;
        $.ajax(settings);
    }
});
Overtrick answered 26/2, 2020 at 3:13 Comment(0)
E
2

None of these answers work if somebody calls .done() after their ajax call because you won't have the success method to attach to the future call back. So if somebody does this:

$.ajax({...someoptions...}).done(mySuccessFunc);

Then mySuccessFunc won't get called on the retry. Here's my solution, which is heavily borrowed from @cjpak's answer here. In my case I want to retry when AWS's API Gateway responds with 502 error.

const RETRY_WAIT = [10 * 1000, 5 * 1000, 2 * 1000];

// This is what tells JQuery to retry $.ajax requests
// Ideas for this borrowed from https://mcmap.net/q/175845/-retry-a-jquery-ajax-request-which-has-callbacks-attached-to-its-deferred
$.ajaxPrefilter(function(opts, originalOpts, jqXHR) {
  if(opts.retryCount === undefined) {
    opts.retryCount = 3;
  }

  // Our own deferred object to handle done/fail callbacks
  let dfd = $.Deferred();

  // If the request works, return normally
  jqXHR.done(dfd.resolve);

  // If the request fails, retry a few times, yet still resolve
  jqXHR.fail((xhr, textStatus, errorThrown) => {
    console.log("Caught error: " + JSON.stringify(xhr) + ", textStatus: " + textStatus + ", errorThrown: " + errorThrown);
    if (xhr && xhr.readyState === 0 && xhr.status === 0 && xhr.statusText === "error") {
      // API Gateway gave up.  Let's retry.
      if (opts.retryCount-- > 0) {
        let retryWait = RETRY_WAIT[opts.retryCount];
        console.log("Retrying after waiting " + retryWait + " ms...");
        setTimeout(() => {
          // Retry with a copied originalOpts with retryCount.
          let newOpts = $.extend({}, originalOpts, {
            retryCount: opts.retryCount
          });
          $.ajax(newOpts).done(dfd.resolve);
        }, retryWait);
      } else {
        alert("Cannot reach the server.  Please check your internet connection and then try again.");
      }
    } else {
      defaultFailFunction(xhr, textStatus, errorThrown); // or you could call dfd.reject if your users call $.ajax().fail()
    }
  });

  // NOW override the jqXHR's promise functions with our deferred
  return dfd.promise(jqXHR);
});

This snippet will back-off and retry after 2 seconds, then 5 seconds, then 10 seconds, which you can edit by modifying the RETRY_WAIT constant.

AWS support suggested we add a retry, since it happens for us only once in a blue moon.

Eternalize answered 20/6, 2018 at 17:15 Comment(1)
I found this to be the most useful of all the answers so far. However, the last line prevents compilation in TypeScript. I don't think you should be returning anything from this function.Moon
G
0

Here is a small plugin for this:

https://github.com/execjosh/jquery-ajax-retry

Auto incrementing timeout would be a good addition to it.

To use it globally just create your own function with $.ajax signature, use there retry api and replace all your $.ajax calls by your new function.

Also you could directly replace $.ajax, but you will not be able to make xhr calls without retry then.

Gandhiism answered 23/5, 2013 at 13:10 Comment(0)
S
0

Here's the method that worked for me for asynchronous loading of libraries:

var jqOnError = function(xhr, textStatus, errorThrown ) {
    if (typeof this.tryCount !== "number") {
      this.tryCount = 1;
    }
    if (textStatus === 'timeout') {
      if (this.tryCount < 3) {  /* hardcoded number */
        this.tryCount++;
        //try again
        $.ajax(this);
        return;
      }
      return;
    }
    if (xhr.status === 500) {
        //handle error
    } else {
        //handle error
    }
};

jQuery.loadScript = function (name, url, callback) {
  if(jQuery[name]){
    callback;
  } else {
    jQuery.ajax({
      name: name,
      url: url,
      dataType: 'script',
      success: callback,
      async: true,
      timeout: 5000, /* hardcoded number (5 sec) */
      error : jqOnError
    });
  }
}

Then just call .load_script from your app and nest your success callback:

$.loadScript('maps', '//maps.google.com/maps/api/js?v=3.23&libraries=geometry&libraries=places&language=&hl=&region=', function(){
    initialize_map();
    loadListeners();
});
Sidky answered 9/3, 2016 at 0:36 Comment(0)
B
0

DemoUsers's answer doesn't work with Zepto, since this in the error function is pointing to Window. (And that way of using 'this' is not secure enough as you don't know how they implement ajax or no need to.)

For Zepto, maybe you could try below, till now it works well for me:

var AjaxRetry = function(retryLimit) {
  this.retryLimit = typeof retryLimit === 'number' ? retryLimit : 0;
  this.tryCount = 0;
  this.params = null;
};
AjaxRetry.prototype.request = function(params, errorCallback) {
  this.tryCount = 0;
  var self = this;
  params.error = function(xhr, textStatus, error) {
    if (textStatus === 'timeout') {
      self.tryCount ++;
      if (self.tryCount <= self.retryLimit) {
        $.ajax(self.params)      
        return;
      }
    }
    errorCallback && errorCallback(xhr, textStatus, error);
  };
  this.params = params;
  $.ajax(this.params);
};
//send an ajax request
new AjaxRetry(2).request(params, function(){});

Use constructor to make sure request is reentrant!

Bermuda answered 25/5, 2016 at 9:37 Comment(0)
O
0

I resolved my specific issue with @vsync 3rd code.

$.ajax = (($oldAjax) => {
    
  var df = $.Deferred();
  
  // on fail, retry by creating a new Ajax deferred
  function check(self, status) {
    console.log("check " + status + " => " + self.retries);
    const shouldRetry = status != 'success' && status != 'parsererror';
    if (shouldRetry && self.retries > 0) {
      setTimeout(() => {
        console.log("retry " + self.retries);
        $.ajax(self);
      }, self.retryInterval || 100);
    }
  }

  function failed(jqXHR, status, e) {
    if (this.retries - 1 <= 0) {
      // 재시도 횟수가 끝나면, 오류 보내기
      df.reject(KfError.convertKfError(jqXHR, this.url));
    } else {
      this.retries --;
      check(this, 'retry', this.retries);
    }
  }

  function done(res, textStatus, jqXHR) {
    if (!res.success) { // 200 코드이지만, 응답에 실패라면 오류로 처리
      if (this.retries - 1 <= 0) {
        df.reject(KfError.createResponseError(res, this.url));
      } else {
        this.retries --;
        check(this, 'retry', this.retries)
      }
    } else {
      df.resolve(res, textStatus, jqXHR);
    }
  }
  return function (settings) {
    $oldAjax(settings)
      .fail(failed)
      .done(done);
    return df;
  };
})($.ajax);

function createRequest(url) {
  return $.ajax({
    type: 'GET',
    url: url,
    timeout: 2000,
    retries: 3,
    retryInterval: 1000
  });
}

$(function () {
  createRequest(Rest.correctUrl('/auth/refres'))
    .then((res) => {
      console.log('ok res');
    })
    .catch((e) => {
      // Finally catch error after retrial.
      console.log(e);
    });
});
Organist answered 29/12, 2022 at 3:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.