6

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?

haindl
  • 3,111
  • 2
  • 25
  • 31
eestein
  • 4,914
  • 8
  • 54
  • 93
  • You can also [register a global Polly policy](https://stackoverflow.com/a/52284010/62600) that Flurl will use by default with every call it makes. – Todd Menier Sep 11 '18 at 20:54

3 Answers3

20

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;
    }
}
Todd Menier
  • 37,557
  • 17
  • 150
  • 173
  • Looks nice, Todd! I had wondered about this kind of approach too. I hadn't initially recommended as I wasn't sure that all flurl calls guarantee to return a `FlurlHttpException`. For instance, https://github.com/tmenier/Flurl/blob/master/src/Flurl.Http.Shared/FlurlClient.cs#L201 (which is public) doesn't trap? But agree this pattern is really nice with the `.GetJsonAsync())`. Nice library, btw. – mountain traveller Nov 23 '16 at 17:39
  • @mountaintraveller If a call completes and the response contains a non-success status, a `FlurlHttpException` is _virtually_ guaranteed. At least that is the intended, documented behavior. But if it's a concern, it certainly can't hurt to keep the other Polly configs in there. (Thanks, and your lib looks pretty sweet too! It's giving me ideas for adding retries to Flurl :)) – Todd Menier Nov 23 '16 at 20:39
  • @mountaintraveller btw, the line you highlighted doesn't trap the error because it gets trapped/rethrown as a `FlurlHttpExcepton` deeper down in [FlurlMessageHandler](https://github.com/tmenier/Flurl/blob/master/src/Flurl.Http.Shared/Configuration/FlurlMessageHandler.cs) – Todd Menier Nov 23 '16 at 20:46
  • @ToddMenier Nice! I hadn't spotted the custom `HttpMessageHandler` registered on `HttpClient` by the default factory. This https://github.com/tmenier/Flurl/blob/master/src/Flurl.Http.Shared/Configuration/FlurlMessageHandler.cs#L25 was a really nice read! – mountain traveller Nov 24 '16 at 00:00
  • @ToddMenier , is this code suppose to compile? I'm getting a cannot convert Task T to Task for the ExecuteAsync – Robin Oct 14 '19 at 10:35
  • @Robin yes it should compile. My example isn't using `HttpResponseMessage` at all, are you sure you're following it correctly? – Todd Menier Oct 14 '19 at 21:02
  • @ToddMenier, thanks, I found the issue, I added a OrResult before Wait, and it changes that to HTTPResponseMessage as a return type. Btw, IsWorthReTrying needs to do null check for .Call,I'm getting null exception if url is not valid. – Robin Oct 15 '19 at 02:15
11

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.

mountain traveller
  • 7,591
  • 33
  • 38
5

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);
}
haindl
  • 3,111
  • 2
  • 25
  • 31
ThomasG
  • 51
  • 1
  • 2