0

I am implementing a retry pattern for Http requests in C# using Polly.

This is the code I am using

return Policy<HttpResponseMessage>
   .Handle<HttpRequestException>()
   .OrTransientHttpStatusCode()                    
   .OrAdditionalTransientHttpStatusCode()
   .Or<TimeoutRejectedException>()
   .WaitAndRetryAsync(
        retryCount: configuration.MaxRetries,
        sleepDurationProvider: (retryIndex, result, context) =>
        {
           if (result.Result is { StatusCode: HttpStatusCode.ServiceUnavailable })
               return some wait time

           if (result.Result is { StatusCode: HttpStatusCode.TooManyRequests } || 
               result.Result is { StatusCode: HttpStatusCode.Forbidden}) 
               return some wait time
  
           return some wait time
        },
        onRetryAsync: (result, span, retryIndex, context) =>
        {
           string requestUrl = result.Result?.RequestMessage?.RequestUri?.AbsoluteUri ?? string.Empty;

           _logger.LogWarning("Transient type: {type} ; Retry index: {index}; Span: {seconds}; Uri: {uri}", type, retryIndex, span.TotalSeconds, requestUrl);
                    
           return Task.CompletedTask;
         });

When the TimeoutRejectedException is triggered, result.Result parameter received in the onRetryAsync method is null, and the result.Exception stores the timeout exception data. In this Exception there is not any information about the endpoint that led to the timeout. As a result, the logging does not have the requestUrl populated.

This is how this is set up for a named HttpClient in the Startup

services
   .AddHttpClient("#id")
   .AddPolicyHandler((sp, _) => new PolicyFactory(sp.GetRequiredService<ILogger<PolicyFactory>>()).Create())

Is there any way that I can set up my policy so that the incoming data provides such information? I want this defined policy to be associated to several named clients (thus the factory), but that the setting and logging is detailed with the values for each incoming request.

CNL
  • 31
  • 1
  • 6
  • HTTP when an exception occurs the HTTP Header Status code is returned with an error number. There is nothing that prevents you from sending error info in the body of the message indicating detail error info. – jdweng Apr 10 '23 at 10:10
  • I cannot change the returning data from the API call. This is a call to an external API which happens to return a timeout exception. – CNL Apr 10 '23 at 10:29
  • If you cannot change the server code and the server return the response so you have no control over what is returned. The body is the HttpResponseMessage and is a class object. With a HTTP error is would be null. You could create a HttpResponseMessage and set a property in the class to a timeout message. – jdweng Apr 10 '23 at 10:35
  • My question was more oriented on setting up the policy in a way that the data provided when the `TimeoutRejectedException` happened would include information about the performed call. I've updated the question in case it was not clear enough. – CNL Apr 10 '23 at 10:45
  • You are returning Policy and the only way of including more info is in the class HttpResponseMessage. – jdweng Apr 10 '23 at 11:29
  • How do you use this policy directly or via HttpClient? In latter case the [AddPolicyHandler has an overload which gives you access to the HttpRequestMessage](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pollyhttpclientbuilderextensions.addpolicyhandler?view=dotnet-plat-ext-3.1#microsoft-extensions-dependencyinjection-pollyhttpclientbuilderextensions-addpolicyhandler(microsoft-extensions-dependencyinjection-ihttpclientbuilder-system-func((system-net-http-httprequestmessage-polly-iasyncpolicy((system-net-http-httpresponsemessage)))))) – Peter Csala Apr 10 '23 at 13:42
  • I've updated the post with how I set this policy in the Startup associated to a named HttpClient. – CNL Apr 10 '23 at 16:11
  • 1
    Thanks @PeterCsala your answer was what I was looking for and it worked like a charm! :) Regarding the already existing question you ask about, it is true that it suggests the same solution (also provided by you) ). I looked at it before writing my own question, but I didn't quite understand that it could be used in the named clients. Also, as I don't have the reputation required, I couldn't ask for this detail in the comments :( Thanks in any case for your help, that also covered the named client part :) – CNL Apr 22 '23 at 09:28

1 Answers1

0

The good news is that the AddPolicyHandler has several overloads. You are using that one which receives an IServiceProvider (sp) and a HttpRequestMessage (_) object as parameter.

So, all you need to do is to pass the HttpRequestMessage to the onRetry(Async) via closure

services
   .AddHttpClient("#id")
   .AddPolicyHandler((sp, request) => Policy<HttpResponseMessage>
     ...
     .WaitAndRetryAsync(
        ...,
        onRetryAsync: (result, span, retryIndex, context) =>
        {
           string requestUrl = request.RequestUri.AbsoluteUri;
           ...
        });

UPDATE #1

however, it is too bad that with this solution I cannot extract the Policy creation to another class and thus reuse it

You don't need to inline the policy definition in the AddPolicyHandler. You can pass the HttpRequestMessage object in the same way as you did with the logger. In the above example I've inlined the policy definition for the sake of simplicity.

Here is a simple example how to pass the request:

Named client decorated with a retry policy

builder.Services
    .AddHttpClient("A")
    .AddPolicyHandler((sp, req) => PolicyHandlers.GetRetryPolicy(sp, req));

The PolicyHandlers class

public static class PolicyHandlers
{
    public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy(IServiceProvider sp, HttpRequestMessage msg)
        => Policy<HttpResponseMessage>
            .HandleResult(res => !res.IsSuccessStatusCode)
            .WaitAndRetryAsync(
            sleepDurations: new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2) },
            onRetry: (dr, ts) => Console.WriteLine(msg.RequestUri.AbsoluteUri));    
    
}

The Named client usage

readonly HttpClient client;
public XYZController(IHttpClientFactory factory)
{
    client = factory.CreateClient("A");
}

[HttpGet]
public async Task<string> Get()
{
    await client.GetAsync("https://httpstat.us/500");
    await client.GetAsync("https://httpstat.us/428");
    return "Finished";
}

The console log

https://httpstat.us/500
https://httpstat.us/500
https://httpstat.us/428
https://httpstat.us/428

This would work as well if you would use the GetRetryPolicy for multiple named clients

builder.Services
    .AddHttpClient("A")
    .AddPolicyHandler((sp, req) => PolicyHandlers.GetRetryPolicy(sp, req));

builder.Services
    .AddHttpClient("B")
    .AddPolicyHandler((sp, req) => PolicyHandlers.GetRetryPolicy(sp, req));

Usage

readonly HttpClient clientA;
readonly HttpClient clientB;
public XYZController(IHttpClientFactory factory)
{
    clientA = factory.CreateClient("A");
    clientB = factory.CreateClient("B");
}

[HttpGet]
public async Task<string> Get()
{
    await clientA.GetAsync("https://httpstat.us/500");
    await clientB.GetAsync("https://httpstat.us/428");
    return "Finished";
}
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • I'll try... however, it is too bad that with this solution I cannot extract the Policy creation to another class and thus reuse it :( – CNL Apr 11 '23 at 12:28
  • @CNL You can use it without I problem. I've used the above example for the sake of simplicity. But let me amend the post. – Peter Csala Apr 11 '23 at 12:48