Polly HandleTransientHttpError 没有捕捉到 HttpRequestException
Polly HandleTransientHttpError not catching HttpRequestException
我在 Startup.ConfigureServices 方法中为我的 HttpClient 创建了一个重试策略。另请注意,默认情况下,asp.net core 2.1 会为 HttpClient 发出的每个调用记录 4 [Information] 行,这些行显示在我的问题末尾的日志中。
services.AddHttpClient("ResilientClient")
.AddPolicyHandler(
Policy.WrapAsync(
PollyRetryPolicies.TransientErrorRetryPolicy(),
Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(60))));
政策定义如下。请注意,我将重试尝试写入日志,因此我将知道是否调用了重试策略。
public static IAsyncPolicy < HttpResponseMessage > TransientErrorRetryPolicy() {
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or < TimeoutRejectedException > ()
.WaitAndRetryAsync(sleepDurations: ExponentialBackoffPolicy.DecorrelatedJitter(3, SEED_DELAY, MAX_DELAY),
onRetry: (message, timespan, attempt, context) => {
context.GetLogger() ? .LogInformation($ "Retrying request to {message?.Result?.RequestMessage?.RequestUri} in {timespan.TotalSeconds} seconds. Retry attempt {attempt}.");
});
}
HandleTransientHttpError() 是一个 Polly 扩展,在其注释中声明:
The conditions configured to be handled are:
• Network failures (as System.Net.Http.HttpRequestException)
我的httpclient用法是这样的:
using (HttpResponseMessage response = await _httpClient.SendAsync(request))
{
response.EnsureSuccessStatusCode();
try
{
string result = await response.Content.ReadAsStringAsync();
if (result == null || result.Trim().Length == 0) {
result = "[]";
}
return JArray.Parse(result);
} catch (Exception ex) {
_logger.LogInformation($ "Failed to read response from {url}. {ex.GetType()}:{ex.Message}");
throw new ActivityException($ "Failed to read response from {url}.", ex);
}
}
捕获了以下日志:
[Information] System.Net.Http.HttpClient.ResilientClient.LogicalHandler: Start processing HTTP request GET https://api.au.... obfuscated
[Information] System.Net.Http.HttpClient.ResilientClient.CustomClientHandler: Sending HTTP request GET https://api.au..... obfuscated
[Information] System.Net.Http.HttpClient.ResilientClient.CustomClientHandler: Received HTTP response after 2421.8895ms - 200
[Information] System.Net.Http.HttpClient.ResilientClient.LogicalHandler: End processing HTTP request after 2422.1636ms - OK
Unknown error responding to request: HttpRequestException:
System.Net.Http.HttpRequestException: Error while copying content to a stream. ---> System.IO.IOException: The server returned an invalid or unrecognized response.
at System.Net.Http.HttpConnection.FillAsync()
at System.Net.Http.HttpConnection.ChunkedEncodingReadStream.CopyToAsyncCore(Stream destination, CancellationToken cancellationToken)
at System.Net.Http.HttpConnection.HttpConnectionResponseContent.SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken)
at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)
--- End of inner exception stack trace ---
at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)
at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
at nd_activity_service.Controllers.ActivityController.GetND(String url) in /codebuild/output/src251819872/src/src/nd-activity-service/Controllers/ActivityController.cs:line 561
Http 调用成功,我可以看到 returns 200 - OK。但是随后抛出了 HttpRequestException。我假设该策略没有被调用,因为 HttpClient 消息管道已经解析,我们可以看到它返回 200 - OK。那么它是如何抛出异常的呢?
我该如何处理?围绕专门处理 HttpRequestExceptions 的方法包装另一个策略?
这个错误似乎是暂时的。它是一个预定的作业,下次调用它时工作。
您的政策是针对 HttpClient
而不是针对 HttpResponseMessage
定义的。
因此,response.EnsureSuccessStatusCode()
将 不会 触发重试,即使您收到例如 428。
如果您从下游系统收到 408 或 5XX 状态代码,HandleTransientHttpError
将触发重试。当 SendAsync
抛出 HttpRequestException
因为您的异常 StackTrace 看起来像这样:
System.Net.Http.HttpRequestException: Error while copying content to a stream.
System.IO.IOException: The server returned an invalid or
unrecognized response.
这就是为什么我有根据的猜测是 HttpContent
class 在您尝试读取响应正文 (ReadAsStringAsync
) 时抛出此异常。
这将不会触发重试,因为您已经在 HttpClient 上定义了策略。
如果您想在 response.EnsureSuccessStatusCode()
抛出 HRE 或 response.Content.ReadAsStringAsync()
抛出 HRE 时重试,那么您必须将整个 http 通信和响应处理逻辑包装到重试中政策。
让我告诉你怎么做。
先用PolicyRegistry代替AddPolicyHandler
:
//services.AddHttpClient("ResilientClient")
// .AddPolicyHandler(
// Policy.WrapAsync(
// TransientErrorRetryPolicy(),
// Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(60))));
services.AddHttpClient("ResilientClient");
var registry = services.AddPolicyRegistry();
registry.Add("retry", Policy.WrapAsync(
TransientErrorRetryPolicy(),
Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(60))));
然后向DI要寄存器,例如:
private readonly IHttpClientFactory factory;
private readonly IReadOnlyPolicyRegistry<string> registry;
public TestController(IHttpClientFactory factory, IReadOnlyPolicyRegistry<string> registry)
{
this.factory = factory;
this.registry = registry;
}
最终检索组合策略并执行 http 调用:
var retryPolicy = registry.Get<IAsyncPolicy<HttpResponseMessage>>("retry");
await retryPolicy.ExecuteAsync(async () => await IssueRequest());
private async Task<HttpResponseMessage> IssueRequest()
{
var _httpClient = factory.CreateClient("ResilientClient");
HttpResponseMessage response = await _httpClient.GetAsync("http://httpstat.us/428");
response.EnsureSuccessStatusCode();
return response;
}
我已经使用 httpstat.us 来模拟 428 响应。
我在 Startup.ConfigureServices 方法中为我的 HttpClient 创建了一个重试策略。另请注意,默认情况下,asp.net core 2.1 会为 HttpClient 发出的每个调用记录 4 [Information] 行,这些行显示在我的问题末尾的日志中。
services.AddHttpClient("ResilientClient")
.AddPolicyHandler(
Policy.WrapAsync(
PollyRetryPolicies.TransientErrorRetryPolicy(),
Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(60))));
政策定义如下。请注意,我将重试尝试写入日志,因此我将知道是否调用了重试策略。
public static IAsyncPolicy < HttpResponseMessage > TransientErrorRetryPolicy() {
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or < TimeoutRejectedException > ()
.WaitAndRetryAsync(sleepDurations: ExponentialBackoffPolicy.DecorrelatedJitter(3, SEED_DELAY, MAX_DELAY),
onRetry: (message, timespan, attempt, context) => {
context.GetLogger() ? .LogInformation($ "Retrying request to {message?.Result?.RequestMessage?.RequestUri} in {timespan.TotalSeconds} seconds. Retry attempt {attempt}.");
});
}
HandleTransientHttpError() 是一个 Polly 扩展,在其注释中声明:
The conditions configured to be handled are: • Network failures (as System.Net.Http.HttpRequestException)
我的httpclient用法是这样的:
using (HttpResponseMessage response = await _httpClient.SendAsync(request))
{
response.EnsureSuccessStatusCode();
try
{
string result = await response.Content.ReadAsStringAsync();
if (result == null || result.Trim().Length == 0) {
result = "[]";
}
return JArray.Parse(result);
} catch (Exception ex) {
_logger.LogInformation($ "Failed to read response from {url}. {ex.GetType()}:{ex.Message}");
throw new ActivityException($ "Failed to read response from {url}.", ex);
}
}
捕获了以下日志:
[Information] System.Net.Http.HttpClient.ResilientClient.LogicalHandler: Start processing HTTP request GET https://api.au.... obfuscated
[Information] System.Net.Http.HttpClient.ResilientClient.CustomClientHandler: Sending HTTP request GET https://api.au..... obfuscated
[Information] System.Net.Http.HttpClient.ResilientClient.CustomClientHandler: Received HTTP response after 2421.8895ms - 200
[Information] System.Net.Http.HttpClient.ResilientClient.LogicalHandler: End processing HTTP request after 2422.1636ms - OK
Unknown error responding to request: HttpRequestException:
System.Net.Http.HttpRequestException: Error while copying content to a stream. ---> System.IO.IOException: The server returned an invalid or unrecognized response.
at System.Net.Http.HttpConnection.FillAsync()
at System.Net.Http.HttpConnection.ChunkedEncodingReadStream.CopyToAsyncCore(Stream destination, CancellationToken cancellationToken)
at System.Net.Http.HttpConnection.HttpConnectionResponseContent.SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken)
at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)
--- End of inner exception stack trace ---
at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)
at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
at nd_activity_service.Controllers.ActivityController.GetND(String url) in /codebuild/output/src251819872/src/src/nd-activity-service/Controllers/ActivityController.cs:line 561
Http 调用成功,我可以看到 returns 200 - OK。但是随后抛出了 HttpRequestException。我假设该策略没有被调用,因为 HttpClient 消息管道已经解析,我们可以看到它返回 200 - OK。那么它是如何抛出异常的呢?
我该如何处理?围绕专门处理 HttpRequestExceptions 的方法包装另一个策略?
这个错误似乎是暂时的。它是一个预定的作业,下次调用它时工作。
您的政策是针对 HttpClient
而不是针对 HttpResponseMessage
定义的。
因此,response.EnsureSuccessStatusCode()
将 不会 触发重试,即使您收到例如 428。
如果您从下游系统收到 408 或 5XX 状态代码,HandleTransientHttpError
将触发重试。当 SendAsync
抛出 HttpRequestException
因为您的异常 StackTrace 看起来像这样:
System.Net.Http.HttpRequestException: Error while copying content to a stream.
System.IO.IOException: The server returned an invalid or unrecognized response.
这就是为什么我有根据的猜测是 HttpContent
class 在您尝试读取响应正文 (ReadAsStringAsync
) 时抛出此异常。
这将不会触发重试,因为您已经在 HttpClient 上定义了策略。
如果您想在 response.EnsureSuccessStatusCode()
抛出 HRE 或 response.Content.ReadAsStringAsync()
抛出 HRE 时重试,那么您必须将整个 http 通信和响应处理逻辑包装到重试中政策。
让我告诉你怎么做。
先用PolicyRegistry代替AddPolicyHandler
:
//services.AddHttpClient("ResilientClient")
// .AddPolicyHandler(
// Policy.WrapAsync(
// TransientErrorRetryPolicy(),
// Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(60))));
services.AddHttpClient("ResilientClient");
var registry = services.AddPolicyRegistry();
registry.Add("retry", Policy.WrapAsync(
TransientErrorRetryPolicy(),
Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(60))));
然后向DI要寄存器,例如:
private readonly IHttpClientFactory factory;
private readonly IReadOnlyPolicyRegistry<string> registry;
public TestController(IHttpClientFactory factory, IReadOnlyPolicyRegistry<string> registry)
{
this.factory = factory;
this.registry = registry;
}
最终检索组合策略并执行 http 调用:
var retryPolicy = registry.Get<IAsyncPolicy<HttpResponseMessage>>("retry");
await retryPolicy.ExecuteAsync(async () => await IssueRequest());
private async Task<HttpResponseMessage> IssueRequest()
{
var _httpClient = factory.CreateClient("ResilientClient");
HttpResponseMessage response = await _httpClient.GetAsync("http://httpstat.us/428");
response.EnsureSuccessStatusCode();
return response;
}
我已经使用 httpstat.us 来模拟 428 响应。