I'm trying to implement a system of retrying ajax requests that fail for a temporary reason. In my case, it is about retrying requests that failed with a 401 status code because the session has expired, after calling a refresh webservice that revives the session.
我正在尝试实现一个系统,该系统重新尝试由于临时原因而失败的ajax请求。在我的例子中,它是关于在调用一个更新会话的webservice之后,重新尝试使用401状态码失败的请求,因为会话已经过期。
The problem is that the "done" callbacks are not called on a successful retry, unlike the "success" ajax option callback that is called. I've made up a simple example below:
问题是,“完成”回调不会在成功的重试中调用,这与调用的“成功”ajax选项回调不同。我举了一个简单的例子:
$.ajaxSetup({statusCode: {
404: function() {
this.url = '/existent_url';
$.ajax(this);
}
}});
$.ajax({
url: '/inexistent_url',
success: function() { alert('success'); }
})
.done(function() {
alert('done');
});
Is there a way to have done-style callbacks called on a successful retry? I know a deferred can't be 'resolved' after it was 'rejected', is it possible to prevent the reject? Or maybe copy the doneList of the original deferred to a new deferred? I'm out of ideas:)
是否有一种方法可以让完成样式的回调调用成功重试?我知道一个延迟不能在被拒绝后“解决”,是否有可能阻止拒绝?或者复制递延到新递延后的原始提交列表?我的想法:
A more realistic example below, where I'm trying to queue up all 401-rejected requests, and retry them after a successful call to /refresh.
下面是一个更实际的示例,我试图将所有401-reject请求排队,并在成功调用/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
}});
The mechanism works, the 401-failed requests get fired a second time, the problem is their 'done' callbacks do not get called, so the applications stalls.
该机制工作,第401-failed请求再次被触发,问题是它们的“完成”回调没有被调用,因此应用程序停止。
5 个解决方案
#1
47
You could use jQuery.ajaxPrefilter
to wrap the jqXHR in another deferred object.
您可以使用jQuery。ajaxPrefilter将jqXHR包装到另一个延迟对象中。
I made an example on jsFiddle
that shows it working, and tried to adapt some of your code to handle the 401 into this version:
我在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);
});
This works because deferred.promise(object)
will actually overwrite all of the "promise methods" on the jqXHR.
这是因为deferred.promise(object)实际上会覆盖jqXHR上的所有“promise方法”。
NOTE: To anyone else finding this, if you are attaching callbacks with success:
and error:
in the ajax options, this snippet will not work the way you expect. It assumes that the only callbacks are the ones attached using the .done(callback)
and .fail(callback)
methods of the jqXHR.
注意:如果您正在附加带有success:和error的回调,那么对于其他找到这个的人来说,这个代码片段在ajax选项中不会按照您期望的方式工作。它假定惟一的回调是使用jqXHR的.done(回调)和.fail(回调)方法附加的回调。
#2
14
As gnarf's answer notes, success and error callbacks will not behave as expected. If anyone is interested here is a version that supports both success
and error
callbacks as well as promises style events.
正如gnarf的回答所指出的,成功和错误回调不会像预期的那样运行。如果有人感兴趣,这里有一个支持成功和错误回调以及承诺样式事件的版本。
$.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);
});
#3
9
I have created a jQuery plugin for this use case. It wraps the logic described in gnarf's answer in a plugin and additionally allows you to specify a timeout to wait before attempting the ajax call again. For example.
我为这个用例创建了一个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!");
});
#4
6
Would something like this work out for you? You just need to return your own Deferred/Promise so that the original one isn't rejected too soon.
像这样的事你能行吗?你只需要返回你自己的延迟/承诺,这样原始的承诺就不会很快被拒绝。
Example/test usage: http://jsfiddle.net/4LT2a/3/
例子/测试使用: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();
}
#5
0
This is a great question that I just faced too.
这也是我刚刚遇到的一个很好的问题。
I was daunted by the accepted answer (from @gnarf), so I figured out a way that I understood easier:
我被公认的答案(来自@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));
}
Basically, I just wrapped the entire Ajax call and its callbacks into one function which can get called recursively.
基本上,我只是将整个Ajax调用及其回调封装到一个函数中,这个函数可以被递归调用。
#1
47
You could use jQuery.ajaxPrefilter
to wrap the jqXHR in another deferred object.
您可以使用jQuery。ajaxPrefilter将jqXHR包装到另一个延迟对象中。
I made an example on jsFiddle
that shows it working, and tried to adapt some of your code to handle the 401 into this version:
我在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);
});
This works because deferred.promise(object)
will actually overwrite all of the "promise methods" on the jqXHR.
这是因为deferred.promise(object)实际上会覆盖jqXHR上的所有“promise方法”。
NOTE: To anyone else finding this, if you are attaching callbacks with success:
and error:
in the ajax options, this snippet will not work the way you expect. It assumes that the only callbacks are the ones attached using the .done(callback)
and .fail(callback)
methods of the jqXHR.
注意:如果您正在附加带有success:和error的回调,那么对于其他找到这个的人来说,这个代码片段在ajax选项中不会按照您期望的方式工作。它假定惟一的回调是使用jqXHR的.done(回调)和.fail(回调)方法附加的回调。
#2
14
As gnarf's answer notes, success and error callbacks will not behave as expected. If anyone is interested here is a version that supports both success
and error
callbacks as well as promises style events.
正如gnarf的回答所指出的,成功和错误回调不会像预期的那样运行。如果有人感兴趣,这里有一个支持成功和错误回调以及承诺样式事件的版本。
$.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);
});
#3
9
I have created a jQuery plugin for this use case. It wraps the logic described in gnarf's answer in a plugin and additionally allows you to specify a timeout to wait before attempting the ajax call again. For example.
我为这个用例创建了一个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!");
});
#4
6
Would something like this work out for you? You just need to return your own Deferred/Promise so that the original one isn't rejected too soon.
像这样的事你能行吗?你只需要返回你自己的延迟/承诺,这样原始的承诺就不会很快被拒绝。
Example/test usage: http://jsfiddle.net/4LT2a/3/
例子/测试使用: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();
}
#5
0
This is a great question that I just faced too.
这也是我刚刚遇到的一个很好的问题。
I was daunted by the accepted answer (from @gnarf), so I figured out a way that I understood easier:
我被公认的答案(来自@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));
}
Basically, I just wrapped the entire Ajax call and its callbacks into one function which can get called recursively.
基本上,我只是将整个Ajax调用及其回调封装到一个函数中,这个函数可以被递归调用。