Search code examples
c#httpresponsepollyflurl

How to use Polly with Flurl.Http?


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?


Solution

  • 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.