Resilient HttpClient with or without Polly

 
 
  • Gérald Barré

Network issues are common. So, you should always handle them in your application. For instance, you can retry the request a few times before giving up. You can also use a cache to avoid making the same request multiple times. You can also use a circuit breaker to avoid making requests to a service that is down.

#Using Polly

Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, Rate-limiting and Fallback. You can use Polly to handle transient errors in your application. To handle transient http errors, you can use the Microsoft.Extensions.Http.Polly NuGet package.

Shell
dotnet add package Microsoft.Extensions.Http.Polly
C#
using Microsoft.Extensions.Http;
using Polly;
using Polly.Extensions.Http;

// Create the policy. Note that I use a simple exponential back-off strategy here,
// but you may also need to use BulkHead and CircuitBreaker policies to improve the
// resilience of your application
var retryPolicy = HttpPolicyExtensions
    .HandleTransientHttpError()
    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

// https://www.meziantou.net/avoid-dns-issues-with-httpclient-in-dotnet.htm
var socketHandler = new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(5),
};

// Use the policy
var pollyHandler = new PolicyHttpMessageHandler(retryPolicy)
{
    InnerHandler = socketHandler,
};

using var httpClient = new HttpClient(pollyHandler);
Console.WriteLine(await httpClient.GetStringAsync("https://www.meziantou.net"));

#With Polly and IHttpClientBuilder

If you use the IHttpClientBuilder to configure your HttpClient, you can use the Microsoft.Extensions.Http.Resilience NuGet package to configure the policy. This package relies on Polly to handle errors. By default, it uses Bulkhead, CircuitBreaker policy, and Retry policies.

Shell
dotnet add package Microsoft.Extensions.Http.Resilience
C#
var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpClientDefaults(http =>
{
    // You can configure the resilience policy if needed
    http.AddStandardResilienceHandler();
});

var app = builder.Build();

#Without using Polly

If you don't want to depend on Polly, you can create your own HttpMessageHandler to handle transient errors. The following code handles transient errors and also the 429 (Too Many Requests) error.

C#
internal static class SharedHttpClient
{
    public static HttpClient Instance { get; } = CreateHttpClient();

    private static HttpClient CreateHttpClient()
    {
        var socketHandler = new SocketsHttpHandler()
        {
            PooledConnectionLifetime = TimeSpan.FromMinutes(5),
        };

        return new HttpClient(new HttpRetryMessageHandler(socketHandler), disposeHandler: true);
    }

    private sealed class HttpRetryMessageHandler : DelegatingHandler
    {
        public HttpRetryMessageHandler(HttpMessageHandler handler)
            : base(handler)
        {
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            const int MaxRetries = 5;
            var defaultDelay = TimeSpan.FromMilliseconds(200);
            for (var i = 1; ; i++, defaultDelay *= 2)
            {
                TimeSpan? delayHint = null;
                HttpResponseMessage? result = null;

                try
                {
                    result = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
                    if (!IsLastAttempt(i) && ((int)result.StatusCode >= 500 || result.StatusCode is System.Net.HttpStatusCode.RequestTimeout or System.Net.HttpStatusCode.TooManyRequests))
                    {
                        // Use "Retry-After" value, if available. Typically, this is sent with
                        // either a 503 (Service Unavailable) or 429 (Too Many Requests):
                        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
                        delayHint = result.Headers.RetryAfter switch
                        {
                            { Date: { } date } => date - DateTimeOffset.UtcNow,
                            { Delta: { } delta } => delta,
                            _ => null,
                        };

                        result.Dispose();
                    }
                    else
                    {
                        return result;
                    }
                }
                catch (HttpRequestException)
                {
                    result?.Dispose();
                    if (IsLastAttempt(i))
                        throw;
                }
                catch (TaskCanceledException ex) when (ex.CancellationToken != cancellationToken) // catch "The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing"
                {
                    result?.Dispose();
                    if (IsLastAttempt(i))
                        throw;
                }

                await Task.Delay(delayHint is { } someDelay && someDelay > TimeSpan.Zero ? someDelay : defaultDelay, cancellationToken).ConfigureAwait(false);

                static bool IsLastAttempt(int i) => i >= MaxRetries;
            }
        }
    }
}

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub