I use the HttpClientFactory together with Refit to make API calls. I want to use Polly for Retry, Circuit Breaker and Fallback, and while it's easy to set up Retry and Circuit Breaker, I struggle with Fallback, as it can only return a fallback value of type HttpResponseMessage
, but what is being returned is a Refit.ApiResponse
. I tried returning a dummy HttpResponseMessage
, but then Refit breaks as it expects certain things to be present, e.g. the HttpRequestMessage
.
Has anyone successfully returned a custom object as a fallback value when using HttpClientFactory
with Refit?
This is the initial setup with Retry:
PolicyBuilder<HttpResponseMessage>? policyBuilder = Policy<HttpResponseMessage>
.Handle<Exception>();
IEnumerable<TimeSpan>? retryDelays = Backoff.DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromSeconds(1), retryCount: 5, fastFirst:true);
AsyncRetryPolicy<HttpResponseMessage>? retryPolicy = policyBuilder.WaitAndRetryAsync(retryDelays, (exception, timeSpan, retryCount, context) =>
{
Debug.WriteLine($"Exception occurred while called Account Service. Retry policy in effect | Retry Attempt: {retryCount} | WaitSeconds: {timeSpan.TotalSeconds}. Exception: {exception.Exception.Message}");
});
This is the circuit breaker:
AsyncCircuitBreakerPolicy<HttpResponseMessage>? circuitBreakerPolicy = policyBuilder
.CircuitBreakerAsync(breakCircuitAfterErrors, TimeSpan.FromMinutes(keepCircuitBreakForMinutes),
(exception, timespan, context) =>
{
// OnBreak, i.e. when circuit state changes to open
Debug.WriteLine(
$"Account Service circuit breaker policy in effect | State changed to Open (blocked). It will remain open for {keepCircuitBreakForMinutes} minutes");
},
(context) =>
{
// OnReset, i.e. when circuit state changes to closed
Debug.WriteLine("Account Service circuit breaker policy in effect | State changed to Closed (normal).");
});
This is the fallback with HttpResponseMessage
, which breaks Refit:
AsyncFallbackPolicy<HttpResponseMessage> fallbackPolicyForCircuitBreaker = Policy<HttpResponseMessage>
.Handle<BrokenCircuitException>()
.FallbackAsync((cancellationToken) =>
{
// In our case we return a null response.
Debug.WriteLine($"The Circuit is Open (blocked). A fallback null value is returned. Try again later.");
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
});
And here I add the policy handlers to the client:
builder.Services.AddHttpClient("AccountService")
.AddPolicyHandler(fallbackPolicyForCircuitBreaker)
.AddPolicyHandler(retryPolicy)
.AddPolicyHandler(circuitBreakerPolicy);
FallbackAsync
has several overloads. One of them looks like this:
public static AsyncFallbackPolicy<TResult> FallbackAsync<TResult>(
this PolicyBuilder<TResult> policyBuilder,
Func<DelegateResult<TResult>, Context, CancellationToken, Task<TResult>> fallbackAction,
Func<DelegateResult<TResult>, Context, Task> onFallbackAsync)
This means in case of fallbackAction
you can access the faulted HttpResponseMessage
via your DelegateResult
.
.FallbackAsync(
fallbackAction:(dr, ctx, ct) => Task.FromResult(new HttpResponseMessage(dr.Result.StatusCode)),
onFallbackAsync:(dr, ctx) => Task.CompletedTask))
But please bear in mind that dr.Result
is only present if there was no exception. Otherwise .Result
will be null
and .Exception
will contain the thrown exception.
If you need to access the request object then you can do that via closure. The AddPolicyHandler
has the following overloads:
public static IHttpClientBuilder AddPolicyHandler (
this IHttpClientBuilder builder,
Func<HttpRequestMessage,IAsyncPolicy<HttpResponseMessage>> policySelector);
public static IHttpClientBuilder AddPolicyHandler (
this IHttpClientBuilder builder,
Func<IServiceProvider,HttpRequestMessage,IAsyncPolicy<HttpResponseMessage>> policySelector);
So, during your registration you can do the following:
.AddPolicyHandler(req => Policy<HttpResponseMessage>
.Handle<BrokenCircuitException>()
.FallbackAsync((...) =>
{
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { RequestMessage = req });
}, ...));