Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reusing HttpRequestMessage in Polly retry policies

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
like image 841
vaindil Avatar asked Oct 21 '25 11:10

vaindil


1 Answers

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():
        # ...
like image 111
hIpPy Avatar answered Oct 24 '25 00:10

hIpPy



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!