Resilient HttpClient with or without Polly
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.
dotnet add package Microsoft.Extensions.Http.Polly
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.
dotnet add package Microsoft.Extensions.Http.Resilience
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.
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!