重试 HttpClient 不成功的请求
Posted
技术标签:
【中文标题】重试 HttpClient 不成功的请求【英文标题】:Retrying HttpClient Unsuccessful Requests 【发布时间】:2013-10-08 23:03:07 【问题描述】:我正在构建一个给定 HttpContent 对象的函数,它将发出请求并在失败时重试。但是,我收到异常说 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 对象或重用它的方法?
【问题讨论】:
其他答案很好实现重试。但这里真正的问题是因为您的 HttpContent 在您的帖子之后被处置。您需要在每次重试之前重新创建 StringContent。你不会有这样的 ObjectDisposedException 没错,这里的异常是由于每次发帖后HttpClient都会处理HttpContent造成的。为每个 Polly 执行克隆 HttpContent 是解决方案。可以在here 找到一些可用的克隆扩展。 【参考方案1】:不要实现包装HttpClient
的重试功能,而是考虑使用在内部执行重试逻辑的HttpMessageHandler
构造HttpClient
。例如:
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<HttpResponseMessage> 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;
// ...
【讨论】:
请注意,此解决方案不适用于瞬态超时。在这种情况下,似乎在 CancellationToken 上请求取消,这可能会导致抛出 TaskCanceledException。 正如@Gabi 提到的,这不适用于超时。似乎SendAsync
代表一个single 请求操作,因此这不是实现重试机制的正确方法。外部方法效果更好。
我实现了这一点,并在自己的脚下开枪,因为如前所述,它不适用于超时。你不应该像我一样忽视其他评论者,你应该听他们的。
我刚刚尝试过这个,它对我来说很糟糕,因为对base.SendAsync
的调用正在处理传递给client.PostAsync
的HttpContent
。所以,IME,你的答案不起作用。 (我得到这个答案的唯一原因是避免复制内容才能重试!:-))
在 .NET Core 中,HttpClient
不再单方面处置HttpContent
,因此部分问题消失了。见:github.com/dotnet/corefx/pull/19082/files【参考方案2】:
ASP.NET Core 2.1 答案
ASP.NET Core 2.1 added support 直接为Polly。这里UnreliableEndpointCallerService
是一个在其构造函数中接受HttpClient
的类。失败的请求将以指数回退重试,以便下一次重试在前一次之后的指数更长的时间内发生:
services
.AddHttpClient<UnreliableEndpointCallerService>()
.AddTransientHttpErrorPolicy(
x => x.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)));
另外,请考虑阅读我的博文"Optimally Configuring HttpClientFactory"。
其他平台回答
此实现使用Polly 以指数回退重试,以便下一次重试在前一次之后的指数更长的时间内进行。如果由于超时而抛出HttpRequestException
或TaskCanceledException
,它也会重试。 Polly 比 Topaz 更容易使用。
public class HttpRetryMessageHandler : DelegatingHandler
public HttpRetryMessageHandler(HttpClientHandler handler) : base(handler)
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken) =>
Policy
.Handle<HttpRequestException>()
.Or<TaskCanceledException>()
.OrResult<HttpResponseMessage>(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");
【讨论】:
自 2016 年 7 月起,Polly 还可以本地处理异常和结果的混合(即自动将某些结果代码视为失败)。因此,上面示例中对 StatusCode 的处理现在可以在 Polly Policy 中本地表达。 Polly readme shows examples @ThiagoSilva 你能不能只在策略中添加一个`.HandleretryCount
重命名为retryAttempt
可能会更好。因为前者意味着重试次数(始终为 5),而后者实际上是指当前的重试尝试,在您的示例中从 1 到 5。
要将DelegatingHandler
方法与超时一起使用,请将总体超时(在所有尝试中)与每次尝试的超时区分开来。 HttpClient 上的 Timeout 属性HttpClient.Timeout
将作为所有重试组合的整体超时。要强制每次尝试超时,请使用包装在 WaitAndRetry 策略中的 Polly TimeoutPolicy。此外,Polly 的 TimeoutPolicy 会抛出 TimeoutRejectedException
。然后 WaitAndRetry 策略应该处理 TimeoutRejectedException
,而不是 TaskCanceledException
。这将每次尝试超时与外部取消和所有重试超时区分开来。
@DennisWelu +1,没错。这正是 Polly 的 TimeoutPolicy 引发不同异常 (TimeoutRejectedException
) 的原因,因此重试策略可以将其与用户取消区分开来。正如我们在 Polly + HttpClientFactory 文档中所建议的那样,您建议 Dennis(独立取消源)的方法实际上就是将 Polly RetryPolicy 与 TimeoutPolicy 嵌套使用的方法:github.com/App-vNext/Polly/wiki/…。【参考方案3】:
当前的答案在所有情况下都不会按预期工作,特别是在请求超时的非常常见的情况下(请参阅我的 cmets 那里)。
此外,它们实施了一种非常幼稚的重试策略 - 很多时候您想要一些更复杂的东西,例如指数退避(这是 Azure 存储客户端 API 中的默认设置)。
我在阅读related blog post 时偶然发现了TOPAZ(还提供了错误的内部重试方法)。这是我想出的:
// sample usage: var response = await RequestAsync(() => httpClient.GetAsync(url));
Task<HttpResponseMessage> RequestAsync(Func<Task<HttpResponseMessage>> requester)
var retryPolicy = new RetryPolicy(transientErrorDetectionStrategy, retryStrategy);
//you can subscribe to the RetryPolicy.Retrying event here to be notified
//of retry attempts (e.g. 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 提供了三种选择:
-
FixedInterval
Incremental
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")))
。
【讨论】:
或查看Polly。一个更轻量级且恕我直言的更清洁(相对于过度设计)的库来做同样的事情! “你不能多次发送同一个请求”。你能扩展一下吗?我有一个委托处理程序可以做到这一点,它似乎工作得很好。我错过了什么吗? @bornfromanegg 当我尝试传递HttpRequestMessage
而不是像现在这样的委托时(通常看起来像 () => client.GetAsync(url)
)然后第一次尝试会起作用,但以下重试会失败,抛出关于无法重用相同请求消息的异常(我不记得确切的异常类型和措辞)
博客目前已关闭,这里是网络存档的链接web.archive.org/web/20150117043128/http://blog.devscrum.net/…【参考方案4】:
复制 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");
【讨论】:
这可行,但对于非字符串 httpcontent 类型确实不方便。像多部分形式等。 同意,我也很想禁止内容的处置。但是您需要在某处构造内容对象,那么为什么不在给定函数内部呢? :) httpcontent 对象的创建由我项目中的另一个组件管理,有时它并不重要,并且请求发出逻辑旨在尽可能通用和可重用。如果每次都创建内容,这有点不合时宜。我通过自己复制 httpcontext 来解决这个问题。 这只会重试从服务器返回的带有一些状态代码的请求。很多时候情况并非如此,并且会抛出异常,例如超时。尽管使用外部包装器,但您的想法是正确的。 @VladL 您不应该在循环中处理和重新创建 HttpClient。 HttpClient(尽管它实现了 IDisposable)旨在在应用程序的生命周期中重复使用,否则它将打开一堆套接字。如果你在负载下运行,你最终会得到 SocketExceptions,这意味着你已经用完了套接字。【参考方案5】:这构建了已接受的答案,但增加了传递重试次数的能力,并增加了向每个请求添加非阻塞延迟/等待时间的能力。它还使用 try catch 来确保在发生异常后继续进行重试。最后,我添加了代码以在 BadRequests 的情况下跳出循环,您不想多次重新发送相同的错误请求。
public class HttpRetryHandler : DelegatingHandler
private int MaxRetries;
private int WaitTime;
public HttpRetryHandler(HttpMessageHandler innerHandler, int maxRetries = 3, int waitSeconds = 0)
: base(innerHandler)
MaxRetries = maxRetries;
WaitTime = waitSeconds * 1000;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
HttpResponseMessage response = null;
for (int i = 0; i < MaxRetries; i++)
try
response = await base.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
return response;
else if(response.StatusCode == HttpStatusCode.BadRequest)
// Don't reattempt a bad request
break;
catch
// Ignore Error As We Will Attempt Again
finally
response.Dispose();
if(WaitTime > 0)
await Task.Delay(WaitTime);
return response;
【讨论】:
这是在 Polly 内部为TimeoutAsync
政策重新发明***【参考方案6】:
使用 RestEase 和 Task,在多次调用(单例)中重用 httpClient 重试时,它会冻结并抛出 TaskCanceledException。 为了解决这个问题,需要在重试之前 Dispose() 失败的响应
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<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
HttpResponseMessage response = null;
for (int i = 0; i < MaxRetries; i++)
response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
return response;
response.Dispose();
return response;
【讨论】:
【参考方案7】:这是我使用 polly 实现的。
nuget
https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly
https://www.nuget.org/packages/Polly
using Polly;
using Polly.Extensions.Http;
//// inside configure service
services.AddHttpClient("RetryHttpClient", c =>
c.BaseAddress = new Uri($"configuration["ExternalApis:MyApi"]/");
c.DefaultRequestHeaders.Add("Accept", "application/json");
c.Timeout = TimeSpan.FromMinutes(5);
c.DefaultRequestHeaders.ConnectionClose = true;
).AddPolicyHandler(GetRetryPolicy());
//// add this method to give retry policy
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
return HttpPolicyExtensions
//// 408,5xx
.HandleTransientHttpError()
//// 404
.OrResult(msg => msg.StatusCode == HttpStatusCode.NotFound)
//// 401
.OrResult(msg => msg.StatusCode == HttpStatusCode.Unauthorized)
//// Retry 3 times, with wait 1,2 and 4 seconds.
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
【讨论】:
【参考方案8】:我在使用单元测试和集成测试时进行了尝试和工作。但是,当我实际从 REST URL 调用时,它卡住了。我找到了this interesting 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;
【讨论】:
【参考方案9】:您还可以参阅为 .NET HttpClient 构建瞬态重试处理程序。 访问参考KARTHIKEYAN VIJAYAKUMAR帖子。
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
UseDefaultCredentials = true,
PreAuthenticate = true,
Proxy = null
;
HttpClient client = new HttpClient(handler);
var result = client.SendAsync(httpRequestMessage).Result.Content
.ReadAsStringAsync().Result;
Console.WriteLine(result.ToString());
Console.ReadKey();
/// <summary>
/// Retry Policy = Error Detection Strategy + Retry Strategy
/// </summary>
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);
/// <summary>
/// This class is responsible for deciding whether the response was an intermittent
/// transient error or not.
/// </summary>
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;
/// <summary>
/// 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.
/// </summary>
class RetryDelegatingHandler : HttpClientHandler
public RetryPolicy retryPolicy get; set;
public RetryDelegatingHandler()
: base()
retryPolicy = CustomRetryPolicy.MakeHttpRetryPolicy();
protected async override Task<HttpResponseMessage> 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;
/// <summary>
/// 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.
/// </summary>
public class HttpRequestExceptionWithStatus : HttpRequestException
public HttpStatusCode StatusCode get; set;
public int CurrentRetryCount get; set;
public HttpRequestExceptionWithStatus()
: base()
public HttpRequestExceptionWithStatus(string message)
: base(message)
public HttpRequestExceptionWithStatus(string message, Exception inner)
: base(message, inner)
【讨论】:
链接的网址无效 链接已修复。【参考方案10】:我几乎有同样的问题。 HttpWebRequest queueing library, which guarantees request delivery 我刚刚更新(参见 EDIT3)避免崩溃的方法,但我仍然需要通用机制来保证消息传递(或在未传递消息的情况下重新传递)。
【讨论】:
【参考方案11】:我有同样的问题并解决了。这是关于“StringContent”/“HttpContent”
请查看 Amogh Natu 的博客,它可以帮助我解决这个问题
这段代码的问题是,当第一次调用 PostAsync 时 成功但失败了,httpContent 对象就被释放了。这是作为 在 HttpClient 类中设计。请参阅此方法中的注释。 尽管这看起来很奇怪,但他们打算这样做,以便用户 不必明确地这样做,也可以避免相同的请求 多次发布。
所以发生的情况是,当第一次调用失败时,httpContent 是 处置,然后由于我们有重试机制,它试图使帖子 再次调用,现在使用已处理的对象,因此这次调用 失败并出现 ObjectDisposedException。
解决这个问题的一个简单方法是不使用变量来存储 httpContent ,而是在制作时直接创建 http 内容 称呼。像这样的。
http://amoghnatu.net/2017/01/12/cannot-access-a-disposed-object-system-net-http-stringcontent-while-having-retry-logic/
【讨论】:
链接无效。【参考方案12】:添加一个同时使用 Polly + 重试策略 + 每次重试超时策略的答案,因为最佳答案没有解决这个问题:
Policy
.Handle<HttpRequestException>()
.Or<TaskCanceledException>()
.Or<TimeoutRejectedException>()
.OrResult<HttpResponseMessage>(x => !x.IsSuccessStatusCode)
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)))
.WrapAsync(
Policy.TimeoutAsync(TimeSpan.FromSeconds(1), delegate (Context ctx, TimeSpan timeSpan, Task task)
// Do some on-timeout action
return Task.CompletedTask;
)
)
.ExecuteAsync(() =>
return httpclient.PostAsync(url, httpRequest);
);
【讨论】:
【参考方案13】: //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;
.........
【讨论】:
处理特定异常总是更好,而不是这样做,最终会导致更多问题以上是关于重试 HttpClient 不成功的请求的主要内容,如果未能解决你的问题,请参考以下文章