When using Flurl as an HttpClient to make requests to a server, requests may fail due to network or other reasons, such as
HttpStatusCode.NotFound,HttpStatusCode.ServiceUnavailable,HttpStatusCode.RequestTimeout, etc. There are many resources online about using HttpClientFactory with Polly for retry logic, but for those already accustomed to Flurl, switching entirely to IHttpClient can be inconvenient. Therefore, this article organizes the implementation of a retry mechanism using Flurl's Polly integration.
Testing Without Polly
- Provide an endpoint for test requests
[Route("api/[controller]")]
[ApiController]
public class PollyController : ControllerBase
{
private readonly ILogger<PollyController> _logger;
public PollyController(ILogger<PollyController> logger)
{
_logger = logger;
}
// GET: api/<PollyController>
[HttpGet]
public IActionResult Get()
{
var random = new Random().Next(0, 8);
switch (random)
{
case 0:
_logger.LogInformation("About to serve a 404");
return StatusCode(StatusCodes.Status404NotFound);
case 1:
_logger.LogInformation("About to serve a 503");
return StatusCode(StatusCodes.Status503ServiceUnavailable);
case 2:
_logger.LogInformation("Sleeping for 10 seconds then serving a 504");
Thread.Sleep(10000);
_logger.LogInformation("About to serve a 504");
return StatusCode(StatusCodes.Status504GatewayTimeout);
default:
_logger.LogInformation("About to correctly serve a 200 response");
return Ok(new {time = DateTime.Now.ToLocalTime()});
}
}
}
- Create a request client
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public async Task<IActionResult> Index()
{
try
{
var time = await "http://127.0.0.1:5000/api/polly"
.GetJsonAsync();
_logger.LogInformation($"App: success - {time.time}");
return View(time.time);
}
catch (Exception e)
{
_logger.LogWarning($"App: failed - {e.Message}");
throw;
}
}
}
- Attempt requests; you can see many failures. This situation is far from ideal, as the server has a high probability of not responding correctly.
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to serve a 404
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to correctly serve a 200 response
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to correctly serve a 200 response
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to correctly serve a 200 response
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to serve a 503
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to serve a 503
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to correctly serve a 200 response
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to serve a 404
Is there a solution for this? The answer is yes. A straightforward idea is to retry the request upon failure. Handling this logic directly in Flurl's return results would be cumbersome and difficult to manage uniformly, which led us to Polly.
Testing with Polly
First, install Polly:
Install-Package PollyBelow is a brief introduction to Polly, followed by the code snippet for the
Policy.
Polly's seven strategies: Retry, Circuit-breaker, Timeout, Bulkhead Isolation, Fallback, and Cache. This article uses the Retry and Timeout strategies.
Retry: Automatically retry on failure; this is a common scenario.
Circuit-breaker: When the system encounters serious problems, it is better to fail fast rather than keep the user/caller waiting. Limiting the consumption of system errors helps recovery. For example, when calling a third-party API that has been unresponsive for a long time (maybe the server is down), continuously retrying can increase system load and potentially affect other tasks. Therefore, when the number of errors exceeds a specified threshold, the circuit should break and wait for a period before resuming. For example:
Policy.Handle<SomeException>().CircuitBreaker(2, TimeSpan.FromMinutes(1));means if the system encounters two such exceptions, it stops and waits for 1 minute before continuing. You can also define callbacks for circuit break and reset.Timeout: If a system waits beyond a certain time, it is reasonable to assume success is impossible. For example, a typical network request completes instantly; if a request exceeds 30 seconds, we can conclude it will not succeed. Therefore, we need to set a timeout to avoid unnecessary waiting. For example:
Policy.Timeout(30, (context, span, task) => {// do something});sets a maximum timeout of 30 seconds; otherwise, it is considered an error and triggers the callback.Bulkhead Isolation: When a fault occurs in one part of the system, it may trigger multiple failed calls, consuming significant resources. Downstream system failures can cause upstream failures and even cascade to system collapse. Therefore, controllable operations should be limited to a fixed-size resource pool to isolate potentially interfering operations. For example:
Policy.Bulkhead(12, context => {// do something});allows a maximum of 12 concurrent threads; if execution is rejected, the callback is executed.Fallback: Some errors are unavoidable, so we need a fallback plan. When an unavoidable error occurs, we should have a reasonable return instead of failure. For example:
Policy.Handle<Whatever>().Fallback<UserAvatar>(() => UserAvatar.GetRandomAvatar());provides a default avatar when the user hasn't uploaded one.Cache: Frequently used resources that do not change often should be cached to improve system responsiveness. Without caching, we would have to check if the resource is in the cache each time we access it. If it is, return it from cache; otherwise, fetch it from the source, cache it, and then return it. Moreover, cache expiration and updates need to be considered. Polly provides caching strategy support to simplify this.
Policy Wrap: An operation may encounter multiple types of faults, each requiring different strategies. These strategies must be combined into a single policy wrap to apply to the same operation. This is Polly's elasticity feature, allowing flexible combination of different strategies.
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Flurl.Http.Configuration;
using Microsoft.Extensions.Logging;
using Polly;
using Polly.Retry;
using Polly.Timeout;
using Polly.Wrap;
namespace WithPollyClient.Services
{
public class Policies
{
private readonly ILogger<Policies> _logger;
public Policies(ILogger<Policies> logger)
{
_logger = logger;
}
private AsyncTimeoutPolicy<HttpResponseMessage> TimeoutPolicy
{
get
{
return Policy.TimeoutAsync<HttpResponseMessage>(3, (context, span, task) =>
{
_logger.LogInformation($"Policy: Timeout delegate fired after {span.Seconds} seconds");
return Task.CompletedTask;
});
}
}
private AsyncRetryPolicy<HttpResponseMessage> RetryPolicy
{
get
{
HttpStatusCode[] retryStatus =
{
HttpStatusCode.NotFound,
HttpStatusCode.ServiceUnavailable,
HttpStatusCode.RequestTimeout
};
return Policy
.HandleResult<HttpResponseMessage>(r => retryStatus.Contains(r.StatusCode))
.Or<TimeoutRejectedException>()
.WaitAndRetryAsync(new[]
{
// Retry 3 times: first after 1 second, second after 2 seconds, third after 4 seconds
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(4)
}, (result, span, count, context) =>
{
_logger.LogInformation($"Policy: Retry delegate fired, attempt {count}");
});
}
}
public AsyncPolicyWrap<HttpResponseMessage> PolicyStrategy =>
Policy.WrapAsync(RetryPolicy, TimeoutPolicy);
}
public class PolicyHandler : DelegatingHandler
{
private readonly Policies _policies;
public PolicyHandler(Policies policies)
{
_policies = policies;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return _policies.PolicyStrategy.ExecuteAsync(ct => base.SendAsync(request, ct), cancellationToken);
}
}
public class PollyHttpClientFactory : DefaultHttpClientFactory
{
private readonly Policies _policies;
public PollyHttpClientFactory(Policies policies)
{
_policies = policies;
}
public override HttpMessageHandler CreateMessageHandler()
{
return new PolicyHandler(_policies)
{
InnerHandler = base.CreateMessageHandler()
};
}
}
}
- Next, configure Flurl in
Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddSingleton<Policies>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var policies = app.ApplicationServices.GetService<Policies>();
FlurlHttp.Configure(setting =>
setting.HttpClientFactory = new PollyHttpClientFactory(policies));
......
- Attempt requests again; the results are now very satisfactory.
WithPollyClient.Services.Policies: Information: Policy: Retry delegate fired, attempt 1
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:14
WithPollyClient.Services.Policies: Information: Policy: Retry delegate fired, attempt 1
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:17
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:22
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:23
WithPollyClient.Services.Policies: Information: Policy: Retry delegate fired, attempt 1
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:25
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:31
WithPollyClient.Services.Policies: Information: Policy: Retry delegate fired, attempt 1
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:34
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:39
WithPollyClient.Services.Policies: Information: Policy: Retry delegate fired, attempt 1
WithPollyClient.Services.Policies: Information: Policy: Timeout delegate fired after 3 seconds
WithPollyClient.Services.Policies: Information: Policy: Retry delegate fired, attempt 2
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:46
Usage in Rich Clients
Sometimes, in WPF or other rich client applications, Flurl is also frequently used. For example:
var time = await Policy
.Handle<FlurlHttpException>()
.OrResult<IFlurlResponse>(r => !r.ResponseMessage.IsSuccessStatusCode)
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(4)
}, (result, span, count, context) =>
{
_logger.LogInformation(count.ToString());
})
.ExecuteAsync(() => "http://127.0.0.1:5000/api/polly".WithTimeout(3).GetAsync())
.ReceiveJson();
_logger.LogInformation($"App: success - {time.time}");
return View(time.time);