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.
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:
builder.Services
.AddHttpClient("A")
.AddPolicyHandler((sp, req) => PolicyHandlers.GetRetryPolicy(sp, req));
PolicyHandlers
classpublic 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));
}
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";
}
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));
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";
}