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 HttpStatusCode
s 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