Search code examples
c#asynchronoustask-parallel-librarypolly

Why does HttpClient continue to fail during subsequent retries using Polly?


I have several methods that require an internet connection of some kind. In the event that the connection fails I want to retry the method for a period of time before failing. As the application can happily continue to run while waiting for a successful response I want to do this asynchronously.

I'm using Polly (5.3.1) to implement asynchronous retry logic leveraging Tasks.

I'm simulating disconnection by starting the process with my Wi-Fi disabled and enabling it during the retry window. I was expecting that after enabling my connection again the method would succeed when retried, what I'm seeing is that the method continues to throw a HttpRequestException as if the connection is down until the retry finishes at which point it throws to the caller.
If I start the method with my Wi-Fi enabled as normal it succeeds straight away.

// Get the HTML of a web page 'async'
public async Task<string> GetHtmlAsync(string url)
{
    using (var client = new HttpClient())
    using (var response = await client.GetAsync(url))
    {
        response.EnsureSuccessStatusCode();
        using (var content = response.Content)
        {
            return await content.ReadAsStringAsync();
        }
    }
}

// Wrapper for Polly to create an async retry policy
public async Task<TResult> RetryAsync<TResult, TException>(Task<TResult> task, int retries, int seconds) where TException : Exception
{
    return await Policy
               .Handle<TException>()
               .WaitAndRetryAsync(retries, wait => TimeSpan.FromSeconds(seconds))
               .ExecuteAsync(async () => await task);
}

// Call the method, it will retry 12 times with a gap of 5 seconds between tries
var html = await RetryAsync<string, HttpRequestException>(GetHtmlAsync("https://www.google.co.uk"), 12, 5);

Why does the method continue to fail even though my connection is enabled and working during subsequent retries?


Solution

  • It's subsequently failing because you're not re-executing anything. Task represents the future result of an asynchronous execution. Subscribing to it only gives you the result, it doesn't rerun the code.

    Think of it like an egg timer that you have started, you can pass it around and everyone can see if it's finished but if it's already finished they'll see that immediately. In your case, since it failed the first time it immediately fails subsequent checks.

    What you want is to retry the invocation:

    public async Task<string> GetHtmlAsync(string url)
    {
        using (var client = new HttpClient())
        using (var response = await client.GetAsync(url))
        {
            response.EnsureSuccessStatusCode();
            using (var content = response.Content)
            {
                return await content.ReadAsStringAsync();
            }
        }
    }
    
    // Wrapper for Polly to create an async retry policy
    public async Task<TResult> RetryAsync<TResult, TException>(
        Func<Task<TResult>> taskInitiator, int retries, int seconds) where TException : Exception
    {
        return await Policy
                   .Handle<TException>()
                   .WaitAndRetryAsync(retries, wait => TimeSpan.FromSeconds(seconds))
                   .ExecuteAsync(async () => await taskInitiator());
    }
    
    // Call the method, it will retry 12 times with a gap of 5 seconds between tries
    var html = await RetryAsync<string, HttpRequestException>(
        () => GetHtmlAsync("https://www.google.co.uk"), 12, 5);