Sunday, November 20, 2022

Resilient HTTP client with Polly and Flurl

Having a resilient HTTP client is a huge benefit if your application depends on third-party API calls. This will helps to avoid transient errors. These errors may be temporary and will be solved quickly. Most of the time these errors occurred due to network issues. 

The below example demonstrates how to implement a HttpClient with multiple retries using Polly and Flurl

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 in a fluent and thread-safe manner.

Flurl is a modern, fluent, asynchronous, testable, portable, buzzword-laden URL builder and HTTP client library.

Creating the Retry Policy 

using Flurl.Http;
using Microsoft.Extensions.Logging;
using Polly;
using Polly.Retry;
namespace RetryHttpClient;
public class RetryPolicyRegistry : IRetryPolicyRegistry
{
private readonly ILogger<RetryPolicyRegistry> _logger;
public RetryPolicyRegistry(ILogger<RetryPolicyRegistry> logger) =>
_logger = logger;
public AsyncRetryPolicy GetTransientRetryPolicy() =>
Policy
.Handle<FlurlHttpException>(IsWorthRetrying)
.WaitAndRetryAsync(
5,
_ => TimeSpan.FromMilliseconds(1000),
OnRetryAsyncFunc);
private Task OnRetryAsyncFunc(Exception exception, TimeSpan timeSpan, int retryCount,
Context context)
{
if (exception is FlurlHttpException httpException)
{
var requestBody = httpException.Call.RequestBody ?? "null";
var requestUrl = httpException.Call.Request;
var errorResponse = httpException.Call.Response;
var responseBody = httpException.GetResponseStringAsync().GetAwaiter().GetResult() ?? "null";
_logger.LogWarning(
"Unable to execute, will retry in {TimeSpan}, attempt #{Attempt}. " +
"Request URL: {RequestUrl}, " +
"Request body: {RequestBody}, " +
"Response: {ErrorResponse}, " +
"Response Body: {ResponseBody}",
timeSpan, retryCount, requestUrl, requestBody, errorResponse, responseBody);
}
else
{
_logger.LogWarning("Unable to execute , will retry in {TimeSpan}, attempt #{Attempt}",
timeSpan, retryCount);
}
return Task.CompletedTask;
}
private static bool IsWorthRetrying(FlurlHttpException ex) {
switch (ex.Call.Response.StatusCode) {
case 408: // RequestTimeout
case 502: // BadGateway
case 503: // ServiceUnavailable
case 504: // GatewayTimeout
return true;
default:
return false;
}
}
}
public interface IRetryPolicyRegistry
{
AsyncRetryPolicy GetTransientRetryPolicy();
}

This IsWorthRetrying method will check returned HTTP status is worth of retry.

And I used  OnRetryAsyncFunc to log the retry information. 


Use the Retry Policy

using Flurl.Http;
using Microsoft.Extensions.Logging;
using Polly;
namespace RetryHttpClient;
public class ApiClient : IApiClient
{
private readonly IRetryPolicyRegistry _retryPolicyRegistry;
private readonly ILogger<ApiClient> _logger;
public ApiClient(IRetryPolicyRegistry retryPolicyRegistry, ILogger<ApiClient> logger)
{
_retryPolicyRegistry = retryPolicyRegistry;
_logger = logger;
}
public async Task<string> GetAsync(string url)
{
_logger.LogInformation("Calling the API Get");
var retry = _retryPolicyRegistry.GetTransientRetryPolicy();
var policyResult = await retry.ExecuteAndCaptureAsync(async () => await url
.GetStringAsync()
.ConfigureAwait(false)).ConfigureAwait(false);
return policyResult.Outcome == OutcomeType.Successful ? policyResult.Result : "Error";
}
}
public interface IApiClient
{
Task<string> GetAsync(string url);
}
view raw ApiClient.cs hosted with ❤ by GitHub

Example usage and results

var result = await apiClient.GetAsync("https://httpstat.us/503").ConfigureAwait(false);
Console.WriteLine($"Hello, World! {result}");
 

Full implementation 

No comments:

Post a Comment