Search code examples
c#dependency-injectiondotnet-httpclientpollyhttpclientfactory

Cannot get Polly retry Http calls when given exceptions are raised


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.


Solution

  • 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.