Reusing HttpRequestMessage in Polly retry policies
Asked Answered
S

1

4

An HttpRequestMessage object can only be used one time; future attempts to use the same object throw an exception. I'm using Polly to retry some requests and I'm hitting this issue. I know how I can clone a request, there are plenty of examples on SO, but I can't figure out how to clone a request and send that new request whenever Polly retries. How can I accomplish this?

These are my policies, for reference. This is a Xamarin app. I want to retry a few times in case of network failures, and if the response is unauthorized I want to re-auth with saved credentials and try the original request again.

public static PolicyWrap<HttpResponseMessage> RetryPolicy
{
    get => WaitAndRetryPolicy.WrapAsync(ReAuthPolicy);
}

private static IAsyncPolicy WaitAndRetryPolicy
{
    get => Policy.Handle<WebException>().WaitAndRetryAsync(4, _ => TimeSpan.FromSeconds(2));
}

private static IAsyncPolicy<HttpResponseMessage> ReAuthPolicy
{
    get => Policy.HandleResult<HttpResponseMessage>(x => x.StatusCode == HttpStatusCode.Unauthorized)
        .RetryAsync((_, __) => CoreService.LogInWithSavedCredsAsync(true));
}

This doesn't work because of the HttpRequestMessage reuse, but it's what I'm trying to accomplish:

var request = new HttpRequestMessage(HttpMethod.Post, "some_endpoint")
{
    Content = new StringContent("some content")
};

request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

var policyResponse = await ConnectivityHelper.RetryPolicy
    .ExecuteAndCaptureAsync(() => _client.SendAsync(request)).ConfigureAwait(false);

// handle outcome
Skit answered 24/5, 2018 at 20:0 Comment(6)
See this q especially this answer. Putting the retry in a DelegatingHandler avoids the cannot-reuse problem. If ASPNET Core and u can wait till 2.1 RTM, consider the new IHttpClientFactory. If so see Polly's doco also.Milicent
@mountaintraveller Thanks, I somehow hadn't seen that one. I'm not sure how to combine it with the policies I already have though, I'm having trouble grasping how the Polly policies should all work together. (This is a Xamarin app, though I think the question is generic enough so I didn't include that.)Skit
Polly doco on IHttpClientFactory discusses when multiple policies are in use. This covers if applying multiple policies with the DelegatingHandler approach without IHttpClientFactory. For discussion of the concept see Steve Gordon's blog. If pre ASPNET Core 2.1, you have to create the chain of DelegatingHandlers manually.Milicent
If your query is generally about the behaviour when combining Polly policies, see Polly wiki on PolicyWrapMilicent
@mountaintraveller Oh, I think I understand now, the missing part in my head was how the DelegatingHandler worked. From what I understand, it's essentially middleware for the HttpClient, correct? So instead of manually calling my Polly policy each time, I configure the whole thing once and pass it to the HttpClient constructor? That makes things so much easier. Now I guess my question is how I can combine the DelegatingHandler approach with the behavior of ExecuteAndCaptureAsync. This is a Xamarin app, by the way.Skit
Yes, DelegatingHandler is middleware for the HttpClient. There isn't a way to combine ExecuteandCaptureAsync() with that - that is, inside a DelegatingHandler. ExecuteandCaptureAsync() changes the return type of the execution; but middleware can't do that.Milicent
N
2

The code to throw InvalidOperationException if an HttpRequestMessage is reused is a validation step within HttpClient itself.

Source code link

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

Source code link

internal bool MarkAsSent()
{
    return Interlocked.Exchange(ref sendStatus, messageAlreadySent) == messageNotYetSent;
}

You can put the polly retry policy in a DelegatingHandler and that works. It also provides a nice SoC (separation of concerns). If, in future, you want to not retry or change retry behavior, you simply remove the DelegatingHandler or change it. Note to dispose off the HttpRequestMessage and intermediate HttpResponseMessages objects. Here is one that I use with good results (retry policy).

Your question is an open-ended, and generally SO is not good for those (see). But here goes. I call this a "reactive" approach as it uses the token right up until its ttl, and fetches the new one. Note that this doesn't incur 401s by using the token ttl.

# gets token with its ttl
tokenService: iTokenService
    # use retry policy in DH here
    httpClient
    string getTokenAsync():
        # calls out for token
        # note: tokens typically have a ttl

# returns cached token till its tll, or gets a new token which is then cached
cachedTokenService: iCachedTokenService
    tokenCached
    tokenTtl
    iTokenService
    
    string getTokenAsync():
        # returns tokenCached or gets a new token based on ttl
        # note: fetches with some buffer before ttl to avoid failures on edge
        # note: buffer as 2x http timeout is good enough

# DH that adds the cached token to the outgoing "work" request
tokenHandler: delegatingHandler
    iCachedTokenService
    task<response> sendAsync(request, ct):
        # gets token, and adds token to request header

# worker service
workService: iWorkService
    # uses tokenHandler DH
    httpClient
    workAsync():
        # ...
Neese answered 21/5, 2021 at 22:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.