当遇到指定的异常时,无法获取 Polly 重试 Http 调用

3

我的服务定义:

var host = new HostBuilder().ConfigureServices(services =>
{
    services
        .AddHttpClient<Downloader>()
        .AddPolicyHandler((services, request) =>
            HttpPolicyExtensions
            .HandleTransientHttpError()
            .Or<SocketException>()
            .Or<HttpRequestException>()
            .WaitAndRetryAsync(
                new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10) },
                onRetry: (outcome, timespan, retryAttempt, context) =>
                {
                    Console.WriteLine($"Delaying {timespan}, retrying {retryAttempt}.");
                }));

    services.AddTransient<Downloader>();

}).Build();

Downloader 的实现:

class Downloader
{
    private HttpClient _client;
    public Downloader(IHttpClientFactory factory)
    {
        _client = factory.CreateClient();
    }

    public Download()
    {
        await _client.GetAsync(new Uri("localhost:8800")); // A port that no application is listening
    }
}

通过这种设置,我希望看到尝试查询端点的三次尝试,并在控制台上打印日志消息(我也曾尝试过使用记录器,但在这里为了简单起见仅使用控制台)。

但实际上我看到的是未处理的异常消息(我只期望在重试和打印日志后才会看到它)。

未经处理的异常:System.Net.Http.HttpRequestException:无法建立连接,因为目标计算机积极拒绝。 (127.0.0.1:8800) ---> System.Net.Sockets.SocketException(10061):无法建立连接,因为目标计算机积极拒绝。


HandleTransientHttpError 捕获 HttpRequestException,因此您不需要为此使用 Or 子句。此外,Or<SocketException> 是不必要的,因为它是内部异常。 - Peter Csala
1
没错,我确实添加了它们以更安全的方式进行操作,但无论如何,我都看不到日志消息。我还尝试了非常激进的等待时间(例如数十个600秒,并相应地增加了客户端的超时时间),这显然是一个明显的等待时间;但我并没有注意到那么长的等待时间! :) 它会在控制台上快速抛出异常并退出。 - Dr. Strangelove
1
明天我会尝试在我的机器上复现它,并让您知道如何修复它。快速浏览时,我无法找到根本原因。 - Peter Csala
谢谢,我会很感激的。 - Dr. Strangelove
1
有趣的是,如果删除 services.AddTransient<Downloader>();,似乎会得到一个与使用 poly 配置的不同客户端实例 (https://www.stevejgordon.co.uk/httpclientfactory-named-typed-clients-aspnetcore),同时将 Downloader(IHttpClientFactory 更改为 Downloader(HttpClient 可以解决这个问题。我有点困惑,因为大多数 MSFT 文档上的示例都使用客户端工厂。虽然我很乐意被证明是错误的,并且错误可能出现在其他地方。 - Dr. Strangelove
显示剩余2条评论
1个回答

6

关于 HttpClient 类型的一些澄清

您可以将多个不同的预配置 HttpClient 注册到 DI 系统中:

  • 命名客户端:这是一个命名的、预配置的 HttpClient,可以通过 IHttpClientFactoryCreate 方法访问
  • 类型化客户端:这是一个预配置的包装器,包装了 HttpClient,可以通过 ITypedHttpClientFactory 或包装器接口访问
  • 命名、类型化客户端:这是一个命名的、预配置的 HttpClient 包装器,可以通过 IHttpClientFactoryITypedHttpClientFactory 访问

AddHttpClient 扩展方法

此方法尝试将工厂注册为单例对象,并将具体类型注册为瞬态对象。因此,您不需要自己将具体类型注册为瞬态或作用域。

此处可找到相关源代码。

命名客户端

您可以通过提供唯一名称,通过 AddHttpClient 注册一个命名客户端。

services.AddHttpClient("UniqueName", client => client.BaseAdress = ...);

您可以通过IHttpClientFactory访问已注册的客户端。
private readonly HttpClient uniqueClient;
public XYZService(IHttpClientFactory clientFactory)
  => uniqueClient = clientFactory.CreateClient("UniqueName");

CreateClient调用未设置名称

如果你没有设置名称调用CreateClient,它将创建一个新的HttpClient,该客户端没有预配置。更精确地说,它并不是由你进行预配置,而是由框架本身带有一些默认设置。

这就是你遇到问题的根本原因,你创建了一个没有使用策略的HttpClient

类型化客户端

你可以通过AddHttpClient<TClient>AddHttpClient<TClient, TImplementation> 多种重载形式注册一个类型化的客户端。

services.AddHttpClient<UniqueClient>(client => client.BaseAdress = ...);
services.AddHttpClient<IUniqueClient, UniqueClient>(client => client.BaseAdress = ...);

您可以通过ITypedHttpClientFactory来访问前者。

private readonly UniqueClient uniqueClient;
public XYZService(ITypedHttpClientFactory<UniqueClient> clientFactory)
  => uniqueClient = clientFactory.CreateClient(new HttpClient());

后者可以通过输入客户端的接口进行访问。
private readonly IUniqueClient uniqueClient;
public XYZService(IUniqueClient client)
  => uniqueClient = client;

在这两种情况下,实现类(UniqueClient)都应该接收一个 HttpClient 参数。
private readonly HttpClient httpClient;
public UniqueClient(HttpClient client)
  => httpClient = client;

命名和类型化的客户端

正如你所看到的,我使用了一个新的HttpClient调用了ITypedHttpClientFactory<UniqueClient>CreateClient方法。(顺便说一句:我也可以使用clientFactory.CreateClient()来调用它。

但它不一定是默认的HttpClient。您也可以检索具有名称的客户端。在这种情况下,您将拥有一个命名的、类型化的客户端。

在这个 SO 主题中,我演示了如何使用此技术多次注册相同的断路器装饰的类型化客户端,以供不同域使用。


2
感谢您对此进行详细说明。 - Dr. Strangelove

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