I have an Azure function that makes a http call to a webapi endpoint. I'm following this example GitHub Polly RetryPolicy so my code has a similar structure. So in Startup.cs i have:
builder.Services.AddPollyPolicies(config); // extension methods setting up Polly retry policies
builder.Services.AddHttpClient("MySender", client =>
{
client.BaseAddress = config.SenderUrl;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
});
My retry policy looks like this:
public static class PollyRegistryExtensions
{
public static IPolicyRegistry<string> AddBasicRetryPolicy(this IPolicyRegistry<string> policyRegistry, IMyConfig config)
{
var retryPolicy = Policy
.Handle<Exception>()
.OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(config.ServiceRetryAttempts, retryCount => TimeSpan.FromMilliseconds(config.ServiceRetryBackOffMilliSeconds), (result, timeSpan, retryCount, context) =>
{
if (!context.TryGetLogger(out var logger)) return;
logger.LogWarning(
$"Service delivery attempt {retryCount} failed, next attempt in {timeSpan.TotalMilliseconds} ms.");
})
.WithPolicyKey(PolicyNames.BasicRetry);
policyRegistry.Add(PolicyNames.BasicRetry, retryPolicy);
return policyRegistry;
}
}
My client sender service receives IReadOnlyPolicyRegistry<string> policyRegistry
and IHttpClientFactory clientFactory
in its constructor. My code calling the client is the following:
var jsonContent = new StringContent(JsonSerializer.Serialize(contentObj),
Encoding.UTF8,
"application/json");
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, "SendEndpoint")
{
Content = jsonContent
};
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var retryPolicy = _policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>(PolicyNames.BasicRetry)
?? Policy.NoOpAsync<HttpResponseMessage>();
var context = new Context($"GetSomeData-{Guid.NewGuid()}", new Dictionary<string, object>
{
{ PolicyContextItems.Logger, _logger }
});
var httpClient = _clientFactory.CreateClient("MySender");
var response = await retryPolicy.ExecuteAsync(ctx =>
httpClient.SendAsync(requestMessage), context);
When I attempt to test this with no endpoint service running then for the first retry attempt, the retry handler is fired and my logger records this first attempt. However, on the second retry attempt i get a error message saying:
The request message was already sent. Cannot send the same request message multiple times
I know that other people have encountered a similar problem (see Retrying HttpClient Unsuccessful Requests and the solution seems to be do what i'm doing (i.e. use HttpClientFactory
). However, i DON'T get this problem if i define my retry policy as part of the configuration in Startup as so:
builder.Services.AddHttpClient("MyService", client =>
{
client.BaseAddress = config.SenderUrl;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}).AddPolicyHandler(GetRetryPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromMilliseconds(1000));
}
and simply call my service as so:
var response = await httpClient.SendAsync(requestMessage);
BUT doing it this way i lose the ability to pass my logger in the retry policy context (which is the whole reason i'm injecting in IReadOnlyPolicyRegistry<string> policyRegistry
- I can not do this at startup). Another benefit is for unit testing - i can simply inject in the same collection with the same policy without copying and pasting a whole bunch of code and making the unit test redundant since i'm no longer testing my service. Having the policy defined in the startup makes this impossible. So my question is, is there a way to not get this duplicate request error using this approach ?