Important
.NET 8
now brings better support for adding resilience to HttpClient
. See Add resilience to an HTTP client and Building resilient cloud services with .NET 8 | .NET Conf 2023.
You should consider adopting the new .NET 8
API instead of using the one presented here.
Every time I use an HttpClient
I end up repeating the same Polly usage pattern in my projects to a set of resilience polices such as:
- retry
- circuit breaker
- timeout
When adding those policies I also end up adding a fallback policy to control what can be returned by the HttpClient
. Eg avoid throwing a TimeoutRejectedException
when the timeout policy triggers.
You will have to add the dotnet-sdk-extensions nuget to your project.
The extension method provided AddFallbackPolicy
is an extension to the IHttpClientBuilder
which is what you use when configuring an HttpClient. This extension will adds a fallback policy to the HttpClient
.
[!NOTE] that the AddFallbackPolicy
adds an opinionated fallback policy which is mainly meant to be used as a fallback for the policies added by the following extension methods:
- Add a timeout policy to an HttpClient
- Add a retry policy to an HttpClient
- Add a circuit breaker policy to an HttpClient
The fallback policy added is configured to handle exceptions and always return a type that derives from HttpResponseMessage
and whose status code is 500
. Each derived type might contain further properties.
The fallback policy is configured to handle the following exceptions:
HttpRequestException
: the fallback response is aExceptionHttpResponseMessage
.TimeoutRejectedException
: the fallback response is aTimeoutHttpResponseMessage
.BrokenCircuitException
andIsolatedCircuitException
: the fallback response is aCircuitBrokenHttpResponseMessage
.TaskCanceledException
: the fallback response is aAbortedHttpResponseMessage
or aTimeoutHttpResponseMessage
if the inner exception isTimeoutException
.
Note
the variable services
in the examples below is of type IServiceCollection
. On the default template
for a Web API you can access it via builder.services
. Example:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
You can add a fallback policy by doing the following:
services
.AddHttpClient("my-http-client")
.AddFallbackPolicy();
The above example is the simplest way to use the extension method. Note that:
-
even though the example shows adding a fallback policy to a named
HttpClient
you can also add it to typedHttpClient
because the extension method works on theIHttpClientBuilder
. -
you can provide a class to handle the events produced by the fallback policy. See Handling events from the fallback policy.
This extension method also enables you to access the events provided by Polly's fallback policy.
You can specify a class to handle the fallback events by doing the following:
services
.AddHttpClient("my-http-client")
.AddFallbackPolicy<MyFallbackEventHandler>();
The MyFallbackEventHandler
must implement the IFallbackPolicyEventHandler
interface.
public class MyFallbackEventHandler : IFallbackPolicyEventHandler
{
private readonly ILogger<MyFallbackEventHandler> _logger;
public MyFallbackEventHandler(ILogger<MyFallbackEventHandler> logger)
{
_logger = logger;
}
public Task OnHttpRequestExceptionFallbackAsync(FallbackEvent fallbackEvent)
{
//do something like logging
_logger.LogInformation($"Fallback response returned due to HttpRequestException for the HttpClient {fallbackEvent.HttpClientName}");
return Task.CompletedTask;
}
public Task OnTimeoutFallbackAsync(FallbackEvent fallbackEvent)
{
//do something like logging
_logger.LogInformation($"Fallback response returned due to timeout for the HttpClient {fallbackEvent.HttpClientName}");
return Task.CompletedTask;
}
public Task OnBrokenCircuitFallbackAsync(FallbackEvent fallbackEvent)
{
//do something like logging
_logger.LogInformation($"Fallback response returned due to broken circuit for the HttpClient {fallbackEvent.HttpClientName}");
return Task.CompletedTask;
}
public Task OnTaskCancelledFallbackAsync(FallbackEvent fallbackEvent)
{
//do something like logging
_logger.LogInformation($"Fallback response returned due to TaskCancelledException for the HttpClient {fallbackEvent.HttpClientName}");
return Task.CompletedTask;
}
}
With the above whenever a fallback is returned from the my-http-client
HttpClient
there will be a log message for it.
There are overloads that enable you to have more control on how the instance that will handle the events is created. For this specic example it doesn't make much sense but could use the overload as follows:
services
.AddHttpClient("my-http-client")
.AddFallbackPolicy(provider =>
{
var loggerFactory = provider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<MyFallbackEventHandler>();
return new MyFallbackEventHandler(logger);
});
For the majority of the cases the overload that accepts a genericy type AddFallbackPolicy<T>
is what is more likely to be used since whatever dependencies you need to provide to the type T
can be passed through the constructor as long as they are added to the IServiceCollection
.
With this fallback in place you reduce exceptions throw by your app. Why does this matter ? Because it helps you avoid doing flow control with exceptions as well as reduces the performance penalty of propagating exceptions through your code.
With this fallback policy you can now write the following:
//httpClient is an HttpClient with the fallback policy applied
var response = await httpClient.GetAsync("/some-path");
if (response.IsSuccessStatusCode)
{
// do something because the request was successful
}
else
{
// do something because the request failed
}
In the above example you don't have to worry about handling exceptions because the fallback policy will handle them and return either an HttpResponseMessage
or a derived type of HttpResponseMessage
with a 500
Internal Server Error
status code.
Furthermore if you need to differentiate the handling of failed requests depending on why it failed then you could do the following:
//httpClient is an HttpClient with the fallback policy applied
var response = await httpClient.GetAsync("/some-path");
if (response.IsSuccessStatusCode)
{
// do something because the request was successful
}
else
{
// do something because the request failed
switch (response)
{
case AbortedHttpResponseMessage abortedHttpResponseMessage:
// do something because the request was aborted (TaskCancelledException)
break;
case CircuitBrokenHttpResponseMessage circuitBrokenHttpResponseMessage:
// do something because the request failed due to the circuit being broken
break;
case ExceptionHttpResponseMessage exceptionHttpResponseMessage:
// do something because the request failed due an HttpRequestException
break;
case TimeoutHttpResponseMessage timeoutHttpResponseMessage:
// do something because the request timed out
// (TimeoutRejectedException or TaskCancelledException with inner exception of TimeoutException)
break;
default: // default will be an instance of type HttpResponseMessage
// do something because the request failed due to the response returned containing a failure status code. Eg 404 Not Found
break;
}
}