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

How is Polly able to resend the same HTTP request?


An HttpRequestMessage can only be sent once. Attempting to resend it results in an InvalidOperationException.

So how is Polly able to circumvent this behavior, in other words, what's going on under the covers when using AddPolicyHandler with a Retry policy? I understand that it uses a DelegatingHandler but how is it able to process the same message multiple times?


Solution

  • TL;DR: It has nothing to do with Polly.


    Let's start with a single example which fails with an InvalidOperationException at the second attempt.

    var request = new HttpRequestMessage(HttpMethod.Get, "https://httpstat.us/500");
    
    var httpClient = new HttpClient(new HttpClientHandler());
    for(int i = 0; i < 3; i++)
    {
        var result = await httpClient.SendAsync(request);
        result.StatusCode.Dump();
    }
    

    If we would move the request object creation inside the for loop then everything would be fine. But let's keep it outside of the for.

    Let's create a simple delegating handler

    public class RetryHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            HttpResponseMessage result = null;
            for(int i = 0; i < 3; i++)
            {
                result = await base.SendAsync(request, cancellationToken);
                "Retried".Dump();
            }
            return result;
        }
    }
    

    and let's use it

    var handler = new RetryHandler();
    handler.InnerHandler = new HttpClientHandler();
    
    var httpClient = new HttpClient(handler);
    var request = new HttpRequestMessage(HttpMethod.Get, "https://httpstat.us/500");
    
    var result = await httpClient.SendAsync(request);
    result.StatusCode.Dump();
    

    This version won't throw any InvalidOperationException.


    So, why does the latter work and former doesn't?

    The InvalidOperationException is thrown by the CheckRequestMessage method of the HttpClient

    private static void CheckRequestMessage(HttpRequestMessage request)
    {
        if (!request.MarkAsSent())
        {
            throw new InvalidOperationException(SR.net_http_client_request_already_sent);
        }
    }
    

    This method is being called by the CheckRequestBeforeSend. And it is the very first command of the SendAsync. This public SendAsync calls its base's SendAsync. Here the base is a HttpMessageInvoker which deals with the HttpMessageHandler.

    Long story short:

    • If you want to reuse the HttpRequestMessage outside the HttpClient then it will fail at the second attempt due to the CheckRequestMessage call
    • If you want to reuse the HttpRequestMessage inside a DelegatingHandler (HttpMessageHandler) then it won't fail because the HttpMessageInvoker does not care about the reuse