重试 httpClient 不成功的请求
本文关键字:请求 不成功 httpClient 重试 | 更新日期: 2023-09-27 18:34:33
我正在构建一个函数,该函数给定一个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对象或重用它吗?
与其实现包装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;
}
// ...
}
}
ASP.NET Core 2.1 答案
ASP.NET 酷睿2.1直接增加了对Polly的支持。这里UnreliableEndpointCallerService
是一个在其构造函数中接受HttpClient
的类。失败的请求将以指数退避重试,以便下一次重试在上一次重试之后的指数级长时间内发生:
services
.AddHttpClient<UnreliableEndpointCallerService>()
.AddTransientHttpErrorPolicy(
x => x.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)));
另外,请考虑阅读我的博客文章"最佳配置HttpClientFactory"。
其他平台回答
此实现使用 Polly 使用指数退避重试,以便下一次重试在上一次重试之后的指数级更长的时间内发生。如果由于超时而引发HttpRequestException
或TaskCanceledException
,它也会重试。波莉比黄玉更容易使用。
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");
}
当前的答案并非在所有情况下都能按预期工作,特别是在请求超时的非常常见的情况下(请参阅我的评论(。
此外,它们还实现了非常幼稚的重试策略 - 很多时候,你需要一些更谨慎的策略,例如指数退避(这是 Azure 存储客户端 API 中的默认设置(。
我在阅读相关博客文章时偶然发现了 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提供了三个选项:
- 固定间隔
- 增量
- 指数退避
例如,下面是 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
处理(感谢您抓住亚历山大·佩平(。例如() => 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");
这就是我使用 polly 实现的。
努盖特
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)));
}
这建立在接受的答案之上,但增加了传递重试量的能力,以及为每个请求添加非阻塞延迟/等待时间的能力。它还使用 try 捕获来确保在发生异常后继续重试。最后,我添加了代码以在 BadRequest 的情况下脱离循环,您不想多次重新发送相同的错误请求。
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 = 1; 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;
}
else
{
_log.WarnFormat("Attempt {0} -> HTTP Status Code = {1}", i, response.StatusCode);
}
}
catch(Exception ex)
{
_log.WarnFormat("Attempt {0} -> Exception = {1}", i, ex);
}
if(WaitTime > 0)
{
await Task.Delay(WaitTime);
}
}
return response;
}
}
使用 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;
}
}
我尝试过并在使用单元和集成测试时工作。但是,当我实际从 REST URL 调用时,它卡住了。我发现了这个有趣的帖子,它解释了为什么它卡在这条线上。
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 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) { }
}
}
我遇到了几乎相同的问题。HttpWebRequest 队列库,保证请求交付我刚刚更新(请参阅 EDIT3(我的方法以避免崩溃,但我仍然需要通用机制来保证消息传递(或在消息未传递的情况下重新传递(。
我有同样的问题并解决了. 这是关于"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/
添加同时使用 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);
});
//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;
.........