Search code examples
c#asp.net-coredotnet-httpclientpollyretry-logic

Polly retry with different url


I am trying to create a solution with polly where I request an other api.
I have a list of URLs to multiple instances of the same service.
I want that when the first request failes, an other should automaticly start with the next url from my list.

Here is an example where i try this behaviour with two static addresses
The Problem with this solution is that the url does not change until i start the next request. I want that the urls changes on every retry

 public static void ConfigureUserServiceClient(this IServiceCollection services)
    {

        _userServiceUri = new Uri("https://localhost:5001");

        services.AddHttpClient("someService", client =>
        {
            client.BaseAddress = _userServiceUri;
            client.DefaultRequestHeaders.Add("Accept", "application/json");
        }).AddPolicyHandler(retryPolicy());
    }

    private static IAsyncPolicy<HttpResponseMessage> retryPolicy()
    {
        return Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.RequestTimeout)
            .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(retryAttempt),
            onRetry: (result, span, ctx) =>
            {
                _userServiceUri = new Uri("https://localhost:5002");
            });
    }

Solution

  • You should consider to use the Fallback policy instead.

    Like this:

    private static HttpClient client = new HttpClient();
    static async Task Main(string[] args)
    {
        var addressIterator = GetUrls().GetEnumerator();
    
        var retryLikePolicy = Policy<string>
            .Handle<HttpRequestException>()
            .FallbackAsync(fallbackAction: async (ct) =>
            {
                if (addressIterator.MoveNext())
                   return await GetData(addressIterator.Current);
                return null;
            });
    
        addressIterator.MoveNext();
        var data = await retryLikePolicy.ExecuteAsync(
           async () => await GetData(addressIterator.Current));
    
        Console.WriteLine("End");
    }
    
    static async Task<string> GetData(string uri)
    {
        Console.WriteLine(uri);
        var response = await client.GetAsync(uri);
        return await response.Content.ReadAsStringAsync();
    }
    
    static IEnumerable<string> GetUrls()
    {
        yield return "http://localhost:5500/index.html";
        yield return "http://localhost:5600/index.html";
        yield return "http://localhost:5700/index.html";
    }
    

    Please note that this code is just for demonstration.


    UPDATE #1: Multiple fallback

    If you have more than one fallback urls then you can alter the above code like this:

    private static HttpClient client = new HttpClient();
    static async Task Main(string[] args)
    {
        var retryInCaseOfHRE = Policy
            .Handle<HttpRequestException>()
            .WaitAndRetryForeverAsync(_ => TimeSpan.FromSeconds(1));
    
        var response = await retryInCaseOfHRE.ExecuteAsync(
             async () => await GetNewAddressAndPerformRequest());
        
        if (response == null)
        {
            Console.WriteLine("All requests failed");
            Environment.Exit(1);
        }
    
        Console.WriteLine("End");
    }
    
    static IEnumerable<string> GetAddresses()
    {
        yield return "http://localhost:5500/index.html";
        yield return "http://localhost:5600/index.html";
        yield return "http://localhost:5700/index.html";
        yield return "http://localhost:5800/index.html";
    }
    
    static readonly IEnumerator<string> AddressIterator = GetAddresses().GetEnumerator();
    
    static async Task<string> GetNewAddressAndPerformRequest()
        => AddressIterator.MoveNext() ? await GetData(AddressIterator.Current) : null;
    
    static async Task<string> GetData(string uri)
    {
        Console.WriteLine(uri);
        var response = await client.GetAsync(uri);
        return await response.Content.ReadAsStringAsync();
    }
    
    • The trick: the retry policy wraps a method which is responsible to retrieve the next url and then call the GetData
      • In other word we need to move the iteration process into the to be wrapped method (GetNewAddressAndPerformRequest)
    • I've replaced the Fallback policy to Retry since we need to perform (potentially) more than 1 fallback actions
    • I've used null to indicate we have run out of fallback urls but it might be a better solution to use a custom exception for that