Cannot get Polly retry Http calls when given exceptions are raised
Asked Answered
M

1

3

My service definition:

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

Implementation of the 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
    }
}

With this setup, I expect to see three attempts of querying the endpoint, with the logging message printed to the console (I've also unsuccessfully tried with a logger, using the console for simplicity here).

Instead of the debugging messages, I see the unhandled exception message (which I only expect to see after the retries and the printed logs).

Unhandled exception: System.Net.Http.HttpRequestException: No connection could be made because the target machine actively refused it. (127.0.0.1:8800) ---> System.Net.Sockets.SocketException (10061): No connection could be made because the target machine actively refused it.

Malnourished answered 13/9, 2022 at 17:56 Comment(7)
HandleTransientHttpError catches HttpRequestException so you don't need an Or clause for this. Also the Or<SocketException> is unnecessary since it is an inner exception.Firecrest
True, I did actually add them to be on the safer side, though regardless, I cannot see the logging messages. I also tried very radical waits (e.g., tens of 600 seconds and increased the client's timeout accordingly) that obviously will be a noticeable wait; I did not notice that long wait! :) It quickly throws the exception on the console and exits.Malnourished
Tomorrow I will try to reproduce it on my machine and let you know how to fix it. At quick glance I can not spot the root cause.Firecrest
thanks, I'd appreciate that.Malnourished
Interestingly removing services.AddTransient<Downloader>(); it seems if it is kept, the instance will get a different client than the one configured with poly (stevejgordon.co.uk/…), also changing Downloader(IHttpClientFactory to Downloader(HttpClient fixes the issue. I am a bit confused since most examples on MSFT docs use client factory. Though I'm happy to proven wrong and shown the error is somewhere else.Malnourished
Yepp, you have found the root cause. I've posted an answer where I have detailed why it caused the problem. I've also tried to give clarity around the different HttpClient types.Firecrest
BTW: Why couldn't you access the Logger? This SO topic details how to do that.Firecrest
F
7

Some clarification around HttpClient types

You can register several different pre-configured HttpClients into the DI system:

  • Named client: It is a named, pre-configured HttpClient which can be accessed through IHttpClientFactory's Create method
  • Typed client: It is a pre-configured wrapper around HttpClient which can be accessed through either the ITypedHttpClientFactory or through the wrapper interface
  • Named, Typed client: It is a named, pre-configured wrapper around HttpClient which can be accessed through IHttpClientFactory and ITypedHttpClientFactory

The AddHttpClient extension method

This method tries to register Factories as singletons and Concrete types as transient objects. Here you can find the related source code. So, you don't need to register the concrete types either as Transient or as Scoped by yourself.

Named clients

You can register a named client via the AddHttpClient by providing a unique name

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

You can access the registered client via the IHttpClientFactory

private readonly HttpClient uniqueClient;
public XYZService(IHttpClientFactory clientFactory)
  => uniqueClient = clientFactory.CreateClient("UniqueName");

CreateClient call without name

If you call the CreateClient without name it will create a new HttpClient which is not pre-configured. More precisely it is not pre-configured by you rather by the framework itself with some default setup.

That's the root cause of your issue, that you have created a HttpClient which is not decorated with the policies.

Typed client

You can register a typed client via the AddHttpClient<TClient> or through the AddHttpClient<TClient, TImplementation> overloads

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

The former can be accessed through the ITypedHttpClientFactory

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

The later can be accessed through the typed client's interface

private readonly IUniqueClient uniqueClient;
public XYZService(IUniqueClient client)
  => uniqueClient = client;

The implementation class (UniqueClient) in both cases should receive an HttpClient as a parameter

private readonly HttpClient httpClient;
public UniqueClient(HttpClient client)
  => httpClient = client;

Named and Typed clients

As you could spot I've called the ITypedHttpClientFactory<UniqueClient>'s CreateClient method with a new HttpClient. (Side note: I could also call it with the clientFactory.CreateClient()).

But it does not have to be a default HttpClient. You can retrieve a named client as well. In that case you would have a named, typed client.

In this SO topic I've demonstrated how to use this technique to register the same Circuit Breaker decorated typed clients multiple times for different domains.

Firecrest answered 14/9, 2022 at 10:30 Comment(1)
Thanks for elaborating on that.Malnourished

© 2022 - 2024 — McMap. All rights reserved.