Polly HandleTransientHttpError 未捕获 HttpRequestException

Posted

技术标签:

【中文标题】Polly HandleTransientHttpError 未捕获 HttpRequestException【英文标题】:Polly HandleTransientHttpError not catching HttpRequestException 【发布时间】:2021-12-13 08:46:00 【问题描述】:

我在 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 扩展,在它的 cmets 中声明:

配置要处理的条件是: • 网络故障(如 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 调用成功,我可以看到它返回 200 - OK。但随后抛出 HttpRequestException。我假设该策略没有被调用,因为 HttpClient 消息管道已经解析,我们可以看到它返回 200 - OK。那么它是如何在此之外抛出异常的呢?

我该如何处理呢?围绕专门处理 HttpRequestExceptions 的方法包装另一个策略?

此错误似乎是暂时的。这是一个预定的作业,在下次调用时工作。

【问题讨论】:

提示:result == null || result.Trim().Length == 0 => string.IsNullOrWhitespace(result) @SeanOB 你知道哪条线抛出了 HRE 吗?我有根据的猜测是await response.Content.ReadAsStringAsync 【参考方案1】:

您的政策是针对HttpClient 而非HttpResponseMessage 定义的。

因此,response.EnsureSuccessStatusCode()不会触发重试,即使您收到例如 428。

如果您从下游系统收到 408 或 5XX 状态代码,HandleTransientHttpError 将触发重试。当SendAsync 抛出HttpRequestException


因为您的异常 StackTrace 如下所示:

System.Net.Http.HttpRequestException:将内容复制到流时出错。

System.IO.IOException:服务器返回无效或 无法识别的响应。

这就是为什么我有根据的猜测是,当您尝试读取响应正文 (ReadAsStringAsync) 时,HttpContent 类会引发此异常。

这将不会触发重试,因为您已在 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 响应。

【讨论】:

我还怀疑 response.Content.ReadAsStringAsync() 导致了异常,但是,它被包装在一个 try - catch 块中并且会记录“无法从 url 读取响应...”出现在我的日志中。所以它比那更早失败。它必须是 SendAsync 或 EnsureSuccessStatusCode 导致 HttpRequestException。它不能是 EnsureSuccessStatusCode,因为 Http 记录器显示 200 响应。所以剩下的唯一原因就是 SendAsync 方法。这就是我希望重试策略捕获异常并重试的原因。 @SeanOB 你没有堆栈跟踪吗?这将有助于确定 HRE 的来源。 你说得对,堆栈跟踪会有所帮助。我已经用应该有用的堆栈跟踪部分更新了我的问题中的日志。它确实是从 HttpClient 类中抛出的。由于我在 HttpClient 上注册的策略似乎无法处理此错误,因此我将实施您在答案中提供的包装策略。非常感谢。

以上是关于Polly HandleTransientHttpError 未捕获 HttpRequestException的主要内容,如果未能解决你的问题,请参考以下文章

Polly的多种弹性策略介绍和简单使用

Polly

弹性和瞬态故障处理库Polly

如何为每个 url 使用 Polly 和 IHttpClientFactory

Polly公共处理 -重试(Retry)

使用最小起订量测试 Polly 重试策略