重试HttpClient不成功的请求

我正在构建一个给出HttpContent对象的函数,它将发出请求并在失败时重试。 但是我得到exception,说HttpContent对象在发出请求后被处理掉。 无论如何都要复制或复制HttpContent对象,以便我可以发出多个请求。

public HttpResponseMessage ExecuteWithRetry(string url, HttpContent content) { HttpResponseMessage result = null; bool success = false; do { using (var client = new HttpClient()) { result = client.PostAsync(url, content).Result; success = result.IsSuccessStatusCode; } } while (!success); return result; } // Works with no exception if first request is successful ExecuteWithRetry("http://www.requestb.in/xfxcva" /*valid url*/, new StringContent("Hello World")); // Throws if request has to be retried ... ExecuteWithRetry("http://www.requestb.in/badurl" /*invalid url*/, new StringContent("Hello World")); 

(显然我不会无限期地尝试,但上面的代码基本上就是我想要的)。

它产生了这个例外

 System.AggregateException: One or more errors occurred. ---> System.ObjectDisposedException: Cannot access a disposed object. Object name: 'System.Net.Http.StringContent'. at System.Net.Http.HttpContent.CheckDisposed() at System.Net.Http.HttpContent.CopyToAsync(Stream stream, TransportContext context) at System.Net.Http.HttpClientHandler.GetRequestStreamCallback(IAsyncResult ar) --- End of inner exception stack trace --- at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions) at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification) at System.Threading.Tasks.Task`1.get_Result() at Submission#8.ExecuteWithRetry(String url, HttpContent content) 

反正有没有复制HttpContent对象或重用它?

不考虑实现包装HttpClient重试function,而是考虑使用HttpMessageHandler构建HttpClient ,该HttpMessageHandler在内部执行重试逻辑。 例如:

 public class RetryHandler : DelegatingHandler { // Strongly consider limiting the number of retries - "retry forever" is // probably not the most user friendly way you could respond to "the // network cable got pulled out." private const int MaxRetries = 3; public RetryHandler(HttpMessageHandler innerHandler) : base(innerHandler) { } protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { HttpResponseMessage response = null; for (int i = 0; i < MaxRetries; i++) { response = await base.SendAsync(request, cancellationToken); if (response.IsSuccessStatusCode) { return response; } } return response; } } public class BusinessLogic { public void FetchSomeThingsSynchronously() { // ... // Consider abstracting this construction to a factory or IoC container using (var client = new HttpClient(new RetryHandler(new HttpClientHandler()))) { myResult = client.PostAsync(yourUri, yourHttpContent).Result; } // ... } } 

ASP.NET Core 2.1答案

ASP.NET Core 2.1直接添加了对Polly的 支持 。 这里UnreliableEndpointCallerService是一个在其构造函数中接受HttpClient的类。 失败的请求将以指数退避重试,以便下一次重试发生在前一次重试之后的指数级更长的时间内:

 services .AddHttpClient() .AddTransientHttpErrorPolicy( x => x.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt))); 

另外,请考虑阅读我的博客文章“Optimally Configure HttpClientFactory” 。

其他平台答案

此实现使用Polly以指数退避重试,以便下一次重试发生在前一次重试之后的指数级更长的时间内。 如果由于超时而抛出HttpRequestExceptionTaskCanceledException它也会重试。 Polly比Topaz更容易使用。

 public class HttpRetryMessageHandler : DelegatingHandler { public HttpRetryMessageHandler(HttpClientHandler handler) : base(handler) {} protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) => Policy .Handle() .Or() .OrResult(x => !x.IsSuccessStatusCode) .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt))) .ExecuteAsync(() => base.SendAsync(request, cancellationToken)); } using (var client = new HttpClient(new HttpRetryMessageHandler(new HttpClientHandler()))) { var result = await client.GetAsync("http://example.com"); } 

在所有情况下,当前答案将无法按预期工作,特别是在请求超时的常见情况下(请参阅我的评论)。

此外,它们实现了一种非常天真的重试策略 – 很多时候你需要更多的东西,比如指数退避(这是Azure存储客户端API中的默认设置)。

我在阅读相关博客文章时偶然发现了TOPAZ (也提供了错误的内部重试方法)。 这就是我想出的:

 // sample usage: var response = await RequestAsync(() => httpClient.GetAsync(url)); Task RequestAsync(Func> requester) { var retryPolicy = new RetryPolicy(transientErrorDetectionStrategy, retryStrategy); //you can subscribe to the RetryPolicy.Retrying event here to be notified //of retry attempts (eg for logging purposes) return retryPolicy.ExecuteAsync(async () => { HttpResponseMessage response; try { response = await requester().ConfigureAwait(false); } catch (TaskCanceledException e) //HttpClient throws this on timeout { //we need to convert it to a different exception //otherwise ExecuteAsync will think we requested cancellation throw new HttpRequestException("Request timed out", e); } //assuming you treat an unsuccessful status code as an error //otherwise just return the respone here return response.EnsureSuccessStatusCode(); }); } 

请注意requester委托参数。 它应该是HttpRequestMessage因为您不能多次发送相同的请求。 至于策略,这取决于您的用例。 例如,瞬态错误检测策略可以简单到:

 private sealed class TransientErrorCatchAllStrategy : ITransientErrorDetectionStrategy { public bool IsTransient(Exception ex) { return true; } } 

至于重试策略,TOPAZ提供三种选择:

  1. FixedInterval
  2. 增加的
  3. ExponentialBackoff

例如,以下是Azure客户端存储库默认使用的TOPAZ等价物:

 int retries = 3; var minBackoff = TimeSpan.FromSeconds(3.0); var maxBackoff = TimeSpan.FromSeconds(120.0); var deltaBackoff= TimeSpan.FromSeconds(4.0); var strategy = new ExponentialBackoff(retries, minBackoff, maxBackoff, deltaBackoff); 

有关更多信息,请参阅http://msdn.microsoft.com/en-us/library/hh680901(v=pandp.50).aspx

编辑请注意,如果您的请求包含HttpContent对象,则每次都必须重新生成它,因为它也将由HttpClient处理(感谢捕获Alexandre Pepin)。 例如() => httpClient.PostAsync(url, new StringContent("foo")))

复制StringContent可能不是最好的主意。 但简单的修改可以解决问题。 只需修改函数并在循环内创建StringContent对象,例如:

 public HttpResponseMessage ExecuteWithRetry(string url, string contentString) { HttpResponseMessage result = null; bool success = false; using (var client = new HttpClient()) { do { result = client.PostAsync(url, new StringContent(contentString)).Result; success = result.IsSuccessStatusCode; } while (!success); } return result; } 

然后打电话给它

 ExecuteWithRetry("http://www.requestb.in/xfxcva" /*valid url*/, "Hello World"); 

我有几乎相同的问题。 HttpWebRequest排队库,它保证我刚刚更新的请求传递 (参见EDIT3)我避免崩溃的方法,但我仍然需要一般机制来保证消息传递(或者在没有传递消息的情况下重新传递)。

我尝试了它,并在使用单元和集成测试时工作。 但是,当我实际从REST URL调用时,它会停滞不前。 我发现这篇有趣的post解释了为什么它会陷入这条线。

 response = await base.SendAsync(request, cancellationToken); 

解决这个问题的方法是在最后添加.ConfigureAwait(false)

 response = await base.SendAsync(request, token).ConfigureAwait(false); 

我还在这里添加了创建链接令牌部分。

 var linkedToken = cancellationToken.CreateLinkedSource(); linkedToken.CancelAfter(new TimeSpan(0, 0, 5, 0)); var token = linkedToken.Token; HttpResponseMessage response = null; for (int i = 0; i < MaxRetries; i++) { response = await base.SendAsync(request, token).ConfigureAwait(false); if (response.IsSuccessStatusCode) { return response; } } return response; 

您还可以参考为.NET HttpClient访问构建一个瞬态重试处理程序。 参考KARTHIKEYAN VIJAYAKUMARpost。

  using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Data.SqlClient; using System.Net.Http; using System.Threading; using System.Diagnostics; using System.Net; using Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling; namespace HttpClientRetyDemo { class Program { static void Main(string[] args) { var url = "http://RestfulUrl"; var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url); var handler = new RetryDelegatingHandler(); handler.UseDefaultCredentials = true; handler.PreAuthenticate = true; handler.Proxy = null; HttpClient client = new HttpClient(handler); var result = client.SendAsync(httpRequestMessage).Result.Content.ReadAsStringAsync().Result; Console.WriteLine(result.ToString()); Console.ReadKey(); } //The retry handler logic is implementing within a Delegating Handler. This has a number of advantages. //An instance of the HttpClient can be initialized with a delegating handler making it super easy to add into the request pipeline. //It also allows you to apply your own custom logic before the HttpClient sends the request, and after it receives the response. //Therefore it provides a perfect mechanism to wrap requests made by the HttpClient with our own custom retry logic. class RetryDelegatingHandler : HttpClientHandler { public RetryPolicy retryPolicy { get; set; } public RetryDelegatingHandler() : base() { retryPolicy = CustomRetryPolicy.MakeHttpRetryPolicy(); } protected async override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { HttpResponseMessage responseMessage = null; var currentRetryCount = 0; //On Retry => increments the retry count retryPolicy.Retrying += (sender, args) => { currentRetryCount = args.CurrentRetryCount; }; try { await retryPolicy.ExecuteAsync(async () => { responseMessage = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); if ((int)responseMessage.StatusCode > 500) { //When it fails after the retries, it would throw the exception throw new HttpRequestExceptionWithStatus(string.Format("Response status code {0} indicates server error", (int)responseMessage.StatusCode)) { StatusCode = responseMessage.StatusCode, CurrentRetryCount = currentRetryCount }; }// returns the response to the main method(from the anonymous method) return responseMessage; }, cancellationToken).ConfigureAwait(false); return responseMessage;// returns from the main method => SendAsync } catch (HttpRequestExceptionWithStatus exception) { if (exception.CurrentRetryCount >= 3) { //write to log } if (responseMessage != null) { return responseMessage; } throw; } catch (Exception) { if (responseMessage != null) { return responseMessage; } throw; } } } //Retry Policy = Error Detection Strategy + Retry Strategy public static class CustomRetryPolicy { public static RetryPolicy MakeHttpRetryPolicy() { //The transient fault application block provides three retry policies that you can use. These are: return new RetryPolicy(strategy, exponentialBackoff); } } //This class is responsible for deciding whether the response was an intermittent transient error or not. public class HttpTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy { public bool IsTransient(Exception ex) { if (ex != null) { HttpRequestExceptionWithStatus httpException; if ((httpException = ex as HttpRequestExceptionWithStatus) != null) { if (httpException.StatusCode == HttpStatusCode.ServiceUnavailable) { return true; } else if (httpException.StatusCode == HttpStatusCode.MethodNotAllowed) { return true; } return false; } } return false; } } //Custom HttpRequestException to allow include additional properties on my exception, which can be used to help determine whether the exception is a transient error or not. public class HttpRequestExceptionWithStatus : HttpRequestException { public HttpRequestExceptionWithStatus() : base() { } public HttpRequestExceptionWithStatus(string message) : base(message) { } public HttpRequestExceptionWithStatus(string message, Exception inner) : base(message, inner) { } public HttpStatusCode StatusCode { get; set; } public int CurrentRetryCount { get; set; } } } } 
  //Could retry say 5 times HttpResponseMessage response; int numberOfRetry = 0; using (var httpClient = new HttpClient()) { do { response = await httpClient.PostAsync(uri, content); numberOfRetry++; } while (response.IsSuccessStatusCode == false | numberOfRetry < 5); } return response; .........