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

11

我有一个应用程序,它请求需要通过access_token进行身份验证的服务。

我的想法是,如果access_token已过期,则使用Polly进行重试。

我在.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;
    });
public interface TwitterApi
{
    [Get("/2/users")]
    Task<string> GetUsers();
}
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;
        }
    }
}

重试策略不起作用!

通过分析问题,我意识到异常没有在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


你注册自定义的 MessageHandler 有什么特别的原因吗?此外,你可以通过传递策略本身来简化 AddPolicyHandler 的调用:.AddPolicyHandler(retryPolicy); - Peter Csala
1
Peter,这就是我想要的(使用AddPolicyHandler)。我注册了一个自定义的MessageHandle,只是为了教学目的。AddPolicyHandler在幕后注册了一个消息处理程序(PolicyHttpMessageHandler)。我的观点是,在那里不会捕获异常,因此策略永远不会执行。 - Marcelo Diniz
4个回答

43

我遇到了一个与 .NET 5 >= 、Polly 和 HttpClient 相关的问题,编译器显示:HttpClientBuilder 不包含 AddPolicyHandler 的定义。 当我将 Nuget 包 Polly.Extensions.Http 更改为 Microsoft.Extensions.Http.Polly 后,问题得到了解决。我知道这并不是本文描述的情况,但对于像我这样寻找答案的其他人可能会有用。


4
非常有用!确实解决了我遇到的问题,谢谢! - Beuz
非常感谢,这也对我有所帮助,你真是个好人。 - Alex Gordon

14
TL;DR: AddPolicyHandlerAddHttpMessageHandler的顺序很重要。 我已经使用HttpClient(而不是Refit)重新创建了问题。 用于测试的类型化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";
    }
}

测试控制器

[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();
    }
}

用于测试的委托处理程序

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;
        }
    }
}

订购 #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);
    });

执行顺序

  1. TestControllerGet
  2. TestClientGet
  3. TestHandlerSendAsync 中的 try
  4. RetryPolicyonRetry
  5. TestHandlerSendAsync 中的 catch
  6. TestControllerGet 因为 HttpRequestException(内部: 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);
    });

执行顺序

  1. TestControllerGet
  2. TestClientGet
  3. TestHandlerSendAsync 中的 try
  4. TestHandlerSendAsync 中的 catch
  5. RetryPolicyonRetry
  6. TestHandlerSendAsync 中的 try
  7. TestHandlerSendAsync 中的 catch
  8. TestControllerGet 因为 HttpRequestException(内部是 SocketException)而失败

因此,在此处触发了重试策略。


5

1. 为什么

在执行策略和委托处理程序时,失败的 HTTP 响应还没有成为异常。它只是一个具有不成功状态的 HttpResponseMessage 实例。Refit 将在请求响应处理的最后一步将此状态转换为异常。

2. 顺序

正如Peter Csala的回答所正确指出的,顺序很重要。当发出请求:

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

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

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

3. 如何

在 HTTP 失败时进行重试的最简单方法是使用来自 Polly.Extensions.Http 的 HttpPolicyExtensions.HandleTransientHttpError()。否则,您将不得不自己检查所有失败的 HTTP 状态码。HandleTransientHttpError 的好处在于它仅在有意义的情况下重试失败的请求,例如 500 或套接字错误。另一方面,如果我们再次尝试,它将不重试 404,因为该资源不存在,并且不太可能重新出现。


这正是我一直在寻找的,帮助我解决了遇到的问题。我一直在尝试处理 ApiException,但没有意识到已经太晚了。谢谢! - Emiel Koning

2

我认为如果我们改变政策

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

to

.HandleResult(x => !x.IsSuccessStatusCode)

或者

.HandleResult(x => _retryStatusCodes.Contains(x.StatusCode))
...
private static readonly ISet<HttpStatusCode> _retryStatusCodes = new HashSet<HttpStatusCode>
        {
            HttpStatusCode.RequestTimeout,
            HttpStatusCode.BadGateway,
            HttpStatusCode.ServiceUnavailable,
            HttpStatusCode.GatewayTimeout,
        };

那么它应该可以工作。

IAsyncPolicy<HttpResponseMessage> retryPolicy = Policy<HttpResponseMessage>

.HandleResult(x => _retryStatusCodes.Contains(x.StatusCode))
.RetryAsync(1, (response, retryCount) =>
{
    System.Diagnostics.Debug.WriteLine($"Polly Retry => Count: {retryCount}");
});

或许 Refit 在后续的某个阶段检查状态码并抛出 ApiException 异常


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接