Polly 策略无法使用“AddPolicyHandler”工作

Mar*_*niz 11 c# dotnet-httpclient refit .net-core polly

我有一个应用程序请求经过身份验证的服务,需要通过access_token.

我的想法是在过期时使用 Polly 重试access_token

我在 .NET Core 3.1 应用程序中使用 Refit (v5.1.67) 和 Polly (v7.2.1)。

服务注册如下:

services.AddTransient<ExampleDelegatingHandler>();

IAsyncPolicy<HttpResponseMessage> retryPolicy = Policy<HttpResponseMessage>
    .Handle<ApiException>()
    .RetryAsync(1, (response, retryCount) =>
    {
        System.Diagnostics.Debug.WriteLine($"Polly Retry => Count: {retryCount}");
    });

services.AddRefitClient<TwitterApi>()
    .ConfigureHttpClient(c =>
    {
        c.BaseAddress = new Uri("https://api.twitter.com/");
    })
    .AddHttpMessageHandler<ExampleDelegatingHandler>()
    .AddPolicyHandler((sp, req) =>
    {
        //this policy does not works, because the exception is not catched on 
        //"Microsoft.Extensions.Http.PolicyHttpMessageHandler" (DelegatingHandler)
        return retryPolicy;
    });
Run Code Online (Sandbox Code Playgroud)
public interface TwitterApi
{
    [Get("/2/users")]
    Task<string> GetUsers();
}
Run Code Online (Sandbox Code Playgroud)
public class ExampleDelegatingHandler : DelegatingHandler
{
    protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        try
        {
            return await base.SendAsync(request, cancellationToken);
        }
        catch (Exception)
        {
            //Why do not catch the exception?
            throw;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

重试策略不起作用!

分析问题后,我意识到异常没有被捕获在 HttpClient 的DelegatingHandler. 由于该AddPolicyHandler语句生成一个DelegatingHandler( PolicyHttpMessageHandler) 来执行策略,并且该处没有捕获异常,因此该策略永远不会执行。我意识到问题只发生在可以发送请求的异步场景中。在同步场景中它可以工作(例如:超时)。

为什么异常没有被捕获在里面DelegatingHandler

我附上了一个模拟 Twitter 调用的示例项目。

https://www.dropbox.com/s/q1797rq1pbjvcls/ConsoleApp2.zip?dl=0

外部参考:

https://github.com/reactiveui/refit#using-httpclientfactory

https://www.hanselman.com/blog/UsingASPNETCore21sHttpClientFactoryWithRefitsRESTLibrary.aspx

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1

And*_*iva 47

我遇到了涉及 .NET 5 >= Polly 和 HttpClient 的问题,编译器显示:HttpClientBuilder 不包含 AddPolicyHandler 的定义。当我将 Nuget 包更改Polly.Extensions.Http为时,我可以修复它Microsoft.Extensions.Http.Polly,我知道这与此处报告的情况不同,但对于像我一样来这里寻找答案的其他人可能有用。

  • 确实非常有用!正是我遇到的问题,谢谢! (4认同)

Pet*_*ala 14

AddPolicyHandlerTL;DR:和的顺序AddHttpMessageHandler确实很重要。


我已经重现了这个问题HttpClient(所以没有改装)。

用于测试的类型化 HttpClient

public interface ITestClient
{
    Task<string> Get();
}

public class TestClient: ITestClient
{
    private readonly HttpClient client;
    public TestClient(HttpClient client)
    {
        this.client = client;
    }
    public async Task<string> Get()
    {
        var resp = await client.GetAsync("http://not-existing.site");
        return "Finished";
    }
}
Run Code Online (Sandbox Code Playgroud)

测试用控制器

[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
    private readonly ITestClient client;

    public TestController(ITestClient client)
    {
        this.client = client;
    }

    [HttpGet]
    public async Task<string> Get()
    {
        return await client.Get();
    }
}
Run Code Online (Sandbox Code Playgroud)

用于测试的 DelegateHandler

public class TestHandler: DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        try
        {
            return await base.SendAsync(request, cancellationToken);
        }
        catch (System.Exception ex)
        {
            _ = ex;
            throw;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

订购 #1 - 处理程序、策略

启动

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddTransient<TestHandler>();
    services.AddHttpClient<ITestClient, TestClient>()
        .AddHttpMessageHandler<TestHandler>() //Handler first
        .AddPolicyHandler(RetryPolicy()); //Policy second
}

private IAsyncPolicy<HttpResponseMessage> RetryPolicy()
    => Policy<HttpResponseMessage>
    .Handle<HttpRequestException>()
    .RetryAsync(1, (resp, count) =>
    {
        Console.WriteLine(resp.Exception);
    });
Run Code Online (Sandbox Code Playgroud)

执行顺序

  1. TestControllerGet
  2. TestClientGet
  3. TestHandlerSendAsynctry
  4. RetryPolicyonRetry
  5. TestHandlerSendAsynccatch
  6. TestControllerGet失败并显示HttpRequestException(inner: SocketException)

因此,这里不会触发重试策略。

排序 #2 - 策略、处理程序

启动

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddTransient<TestHandler>();
    services.AddHttpClient<ITestClient, TestClient>()
        .AddPolicyHandler(RetryPolicy()) //Policy first
        .AddHttpMessageHandler<TestHandler>(); //Handler second
}

private IAsyncPolicy<HttpResponseMessage> RetryPolicy()
    => Policy<HttpResponseMessage>
    .Handle<HttpRequestException>()
    .RetryAsync(1, (resp, count) =>
    {
        Console.WriteLine(resp.Exception);
    });
Run Code Online (Sandbox Code Playgroud)

执行顺序

  1. TestControllerGet
  2. TestClientGet
  3. TestHandlerSendAsynctry
  4. TestHandlerSendAsynccatch
  5. RetryPolicyonRetry
  6. TestHandlerSendAsynctry
  7. TestHandlerSendAsynccatch
  8. TestControllerGet失败并显示HttpRequestException(inner: SocketException)

因此,这里重试政策已被解雇。


Sna*_*akE 5

1. 为什么

在执行策略和委派处理程序时,失败的 HTTP 响应也不是例外。这只是一个HttpResponseMessage不成功的例子。Refit 将此状态转换为异常,作为请求-响应处理的最后一步。

2. 订单

正如Peter Csala 的回答中正确指出的那样,顺序很重要。当提出请求时:

  1. Refit将参数序列化成anHttpRequestMessage并传递给HttpClient
  2. HttpClient做初步准备
  3. HttpClient按照添加到客户端的顺序通过处理程序和策略运行请求消息
  4. 结果消息被发送到服务器
  5. 服务器的响应被转换为HttpResponseMessage对象
  6. 该对象通过相同序列的处理程序和策略冒泡,但顺序相反
  7. HttpClient进行最终处理并将结果返回给Refit
  8. Refit 将任何错误转换为ApiExceptions

因此,重试策略将重新运行在其之后添加的所有内容,但在其之前的内容将仅执行一次。

因此,如果您希望access_token在每次重试时重新生成令牌,则必须在重试策略之后注册创建令牌的委托处理程序。

3. 如何

重试 HTTP 失败的最简单方法是使用HttpPolicyExtensions.HandleTransientHttpError()from Polly.Extensions.Http。否则,您必须自己检查所有失败的 HTTP 状态代码。这样做的好处HandleTransientHttpError是它只重试那些有意义的失败,例如 500 或套接字错误。另一方面,例如,它不会重试 404,因为资源不存在,并且如果我们重试,该资源不太可能重新出现。