Resilient HttpClient with or without Polly

 
 
  • Gérald Barré

Network issues are common, so you should always handle them in your application. For example, you can retry requests a few times before giving up, use a cache to avoid duplicate requests, or use a circuit breaker to avoid calling a service that is down.

#Using Polly

Polly is a .NET resilience and transient-fault-handling library that lets developers express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, Rate-limiting, and Fallback. To handle transient HTTP errors, 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 IHttpClientBuilder to configure your HttpClient, you can use the Microsoft.Extensions.Http.Resilience NuGet package to configure resilience policies. This package relies on Polly and, by default, applies Bulkhead, CircuitBreaker, 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 prefer not to depend on Polly, you can implement your own HttpMessageHandler to handle transient errors. The following code handles transient errors and the 429 (Too Many Requests) status code.

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?