Currently I have this request:
await url
.SetQueryParams(queryString)
.SetClaimsToken()
.GetJsonAsync<T>()
I'd like to start using Polly (https://github.com/App-vNext/Polly) now to handle retries and provide a better user experience. For instance, not "hanging up" on the user on the first attempt due to bad network connection. This is the example I was trying to use:
int[] httpStatusCodesWorthRetrying = { 408, 500, 502, 503, 504 };
Policy
.Handle<HttpException>()
.OrResult<HttpResponse>(r => httpStatusCodesWorthRetrying.Contains(r.StatusCode))
.WaitAndRetryAsync(new[] {
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(3)
})
.ExecuteAsync( await url... )
But it requires HttpResponse to be the return type. As you can see from my Flurl example, it's returning T, even though it is an HttpResponse. The T is just the type used to deserialize the StringContent.
This first example is not working at all since I'm using it inside a PCL and I can't get a reference to System.Web there. So I tried this:
Policy
.HandleResult(HttpStatusCode.InternalServerError)
.OrResult(HttpStatusCode.BadGateway)
.OrResult(HttpStatusCode.BadRequest)
.WaitAndRetryAsync(new[] {
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(3)
})
.ExecuteAsync(async () =>
{
await url...
});
But this one also doesn't work because Polly expects HttpStatusCode as return type. So my question is: How can I tell polly to handle those HttpStatusCodes and still allow my return of type T?
You shouldn't need to break from using convenience methods like GetJsonAsync<T>(), because Flurl throws an exception on non-2XX responses (or however you configure it), which should allow it to play very nicely with Polly. Just remove the .Handle<HttpException> and .OrResult<HttpResponse> parts in your original code and handle FlurlHttpException instead:
T poco = await Policy
.Handle<FlurlHttpException>(ex => httpStatusCodesWorthRetrying.Contains((int)ex.Call.Response.StatusCode))
.WaitAndRetryAsync(...)
.ExecuteAsync(() => url
.SetQueryParams(queryString)
.SetClaimsToken()
.GetJsonAsync<T>());
And just a suggestion for cleaning that up further:
T poco = await Policy
.Handle<FlurlHttpException>(IsWorthRetrying)
.WaitAndRetryAsync(...)
.ExecuteAsync(() => url
.SetQueryParams(queryString)
.SetClaimsToken()
.GetJsonAsync<T>());
private bool IsWorthRetrying(FlurlHttpException ex) {
switch ((int)ex.Call.Response.StatusCode) {
case 408:
case 500:
case 502:
case 504:
return true;
default:
return false;
}
}
Polly can interpret any value returned by a delegate executed through a policy, as a fault. However, as you observed, the call to .GetJsonAsync<T>() in your posted example:
await url
.SetQueryParams(queryString)
.SetClaimsToken()
.GetJsonAsync<T>()
is returning T. The call hides HttpResponseMessage by going straight to Json deserialization to T.
You'd need to use an overload in flurl which returns something around HttpResponseMessage. I haven't used flurl, but this overload returning Task<HttpResponseMessage> looks promising. You could probably do something like:
List<int> httpStatusCodesWorthRetrying = new List<int>(new[] {408, 500, 502, 503, 504});
HttpResponseMessage response = await Policy
.Handle<HttpRequestException>()
.Or<OtherExceptions>() // add other exceptions if you find your call may throw them, eg FlurlHttpException
.OrResult<HttpResponseMessage>(r => httpStatusCodesWorthRetrying.Contains((int)r.StatusCode))
.WaitAndRetryAsync(new[] {
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(3)
})
.ExecuteAsync(() =>
url
.SetQueryParams(queryString)
.SetClaimsToken()
.GetAsync()
);
T responseAsT = await Task.FromResult(response).ReceiveJson<T>();
The call to .ReceiveJson<T>() at the end is suggested simply be comparing the flurl source code for your original call .GetJsonAsync<T>() here with the substituted .GetAsync(); here.
Of course you could wrap it all into a concise extension helper method on flurl, perhaps something like this:
async T GetJsonAsyncResiliently<T>(this IFlurlClient client, Policy policy) // OR (if preferred): this Url url instead of IFlurlClient client
{
return await Task.FromResult(policy.ExecuteAsync(() => client.GetAsync())).ReceiveJson<T>();
}
EDIT: I may have pointed to the wrong flurl overloads for your case, in pointing to methods on IFlurlClient. However, a parallel set of extension methods exist within flurl on Url and string, so the same principles apply.
Configure Flurl by setting the HttpClientFactory that can be configured with Polly and create a custom HttpClientFactory:
public class MyCustomHttpClientFactory : DefaultHttpClientFactory, IMyCustomHttpClientFactory
{
private readonly HttpClient _httpClient;
public MyCustomHttpClientFactory(HttpClient httpClient)
{
_httpClient = httpClient;
}
public override HttpClient CreateHttpClient(HttpMessageHandler handler)
{
return _httpClient;
}
}
Register that Service in ConfigureServices with:
public void ConfigureServices(IServiceCollection services)
{
services
.AddHttpClient<IMyCustomHttpClientFactory, MyCustomHttpClientFactory>()
.SetHandlerLifetime(...)
.AddPolicyHandler(....);
}
And assign that Factory to Flurl:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Get HttpClientFactory and Configure Flurl to use it.
var factory = (IMyCustomHttpClientFactory)app.ApplicationServices.GetService(typeof(IMyCustomHttpClientFactory));
FlurlHttp.Configure((settings) => settings.HttpClientFactory = factory);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With