我有一个应用程序,它请求经过身份验证的服务,其中需要传递access_token
。
如果access_token
过期,我的想法是使用Polly重试。
我在.NET Core 3.1应用程序中使用重新安装(v.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
我遇到了一个涉及.NET 5>的问题对于Polly和HttpClient,编译器显示:HttpClientBuilder不包含AddPolicyHandler的定义。当我将Nuget包Polly.Extensions.Http
更改为Microsoft.Extensions.Http.Polly
时,我可以修复它,我知道这与这里报道的情况不同,但它可能对其他来这里找到这个答案的人有用,比如我。
TL;DR:AddPolicyHandler
和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";
}
}
测试控制器
[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();
}
}
用于测试的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;
}
}
}
订购#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);
});
执行命令
TestController
的Get
TestClient
的Get
TestHandler
的SendAsync
的try
RetryPolicy
的onRetry
TestHandler
的SendAsync
的catch
TestController
的Get
与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);
});
执行命令
TestController
的Get
TestClient
的Get
TestHandler
的SendAsync
的try
TestHandler
的SendAsync
的catch
RetryPolicy
的onRetry
TestHandler
的SendAsync
的try
TestHandler
的SendAsync
的catch
TestController
的Get
与HttpRequestException
一起失败(内部:SocketException
)
因此,此处已启动重试策略。
1。为什么
在执行策略和委派处理程序时,失败的HTTP响应还不是例外。它只是HttpResponseMessage
的一个不成功的实例。重新安装将此状态转换为异常,作为请求-响应处理的最后一步。
2.订单
正如Peter Csala的回答中正确指出的那样,秩序很重要。提出请求时:
- 重新装配将参数串行化为
HttpRequestMessage
并将其传递给HttpClient
HttpClient
做前期准备HttpClient
按照将请求消息添加到客户端的顺序,通过处理程序和策略运行请求消息- 将生成的消息发送到服务器
- 服务器的响应被转换为
HttpResponseMessage
对象 - 该对象通过相同的处理程序和策略序列冒泡,但顺序相反
HttpClient
进行最终处理并将结果返回给"重新装配">- 重新安装将任何错误转换为
ApiException
s
因此,重试策略将重新运行之后添加的所有内容,但之前的内容将只执行一次。
因此,如果您希望在每次重试时重新生成access_token
,则创建令牌的委派处理程序必须在重试策略之后注册。
3.如何
重试HTTP失败的最简单方法是使用Polly.Extensions.Http
中的HttpPolicyExtensions.HandleTransientHttpError()
。否则,您将不得不自己检查所有失败的HTTP状态代码。HandleTransientHttpError
的好处是,它只在有意义的失败时重试,比如500或套接字错误。另一方面,它不会重试404,例如,因为资源不在那里,如果我们重试,就不太可能再次出现。
我认为如果我们更改策略
IAsyncPolicy<HttpResponseMessage> retryPolicy = Policy<HttpResponseMessage>
.Handle<ApiException>()
.RetryAsync(1, (response, retryCount) =>
{
System.Diagnostics.Debug.WriteLine($"Polly Retry => Count: {retryCount}");
});
至
.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}");
});
也许重新安装会检查状态代码,并在稍后阶段的上抛出ApiException