71

I am building a function that given an HttpContent Object, will issues request and retry on failure. However I get exceptions saying that HttpContent Object is disposed after issuing the request. Is there anyway to copy or duplicate the HttpContent Object so that I can issue multiple requests.

 public HttpResponseMessage ExecuteWithRetry(string url, HttpContent content)
 {
  HttpResponseMessage result = null;
  bool success = false;
  do
  {
      using (var client = new HttpClient())
      {
          result = client.PostAsync(url, content).Result;
          success = result.IsSuccessStatusCode;
      }
  }
  while (!success);

 return result;
} 

// Works with no exception if first request is successful
ExecuteWithRetry("http://www.requestb.in/xfxcva" /*valid url*/, new StringContent("Hello World"));
// Throws if request has to be retried ...
ExecuteWithRetry("http://www.requestb.in/badurl" /*invalid url*/, new StringContent("Hello World"));

(Obviously I don't try indefinitely but the code above is essentially what i want).

It yields this exception

System.AggregateException: One or more errors occurred. ---> System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Net.Http.StringContent'.
   at System.Net.Http.HttpContent.CheckDisposed()
   at System.Net.Http.HttpContent.CopyToAsync(Stream stream, TransportContext context)
   at System.Net.Http.HttpClientHandler.GetRequestStreamCallback(IAsyncResult ar)
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at System.Threading.Tasks.Task`1.get_Result()
   at Submission#8.ExecuteWithRetry(String url, HttpContent content)

Is there anyway to duplicate an HttpContent Object or reuse it?

Darrel Miller
  • 139,164
  • 32
  • 194
  • 243
samirahmed
  • 1,219
  • 2
  • 12
  • 16
  • 9
    Other answers are good to implement a retry. But the real problem here is because your HttpContent is disposed after your post. You need to re-create the StringContent before each retry. You wont have a ObjectDisposedException like this – Alexandre Pepin May 03 '16 at 17:52
  • Exactly, the exception here is caused by HttpContent being disposed by HttpClient after each post. Cloning the HttpContent for each Polly execution is the solution. Some working cloning extensions can be found [here](https://stackoverflow.com/questions/18000583/re-send-httprequestmessage-exception). – Jan Matousek Feb 15 '22 at 21:57

13 Answers13

95

Instead of implementing retry functionality that wraps the HttpClient, consider constructing the HttpClient with a HttpMessageHandler that performs the retry logic internally. For example:

public class RetryHandler : DelegatingHandler
{
    // Strongly consider limiting the number of retries - "retry forever" is
    // probably not the most user friendly way you could respond to "the
    // network cable got pulled out."
    private const int MaxRetries = 3;

    public RetryHandler(HttpMessageHandler innerHandler)
        : base(innerHandler)
    { }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        HttpResponseMessage response = null;
        for (int i = 0; i < MaxRetries; i++)
        {
            response = await base.SendAsync(request, cancellationToken);
            if (response.IsSuccessStatusCode) {
                return response;
            }
        }

        return response;
    }
}

public class BusinessLogic
{
    public void FetchSomeThingsSynchronously()
    {
        // ...

        // Consider abstracting this construction to a factory or IoC container
        using (var client = new HttpClient(new RetryHandler(new HttpClientHandler())))
        {
            myResult = client.PostAsync(yourUri, yourHttpContent).Result;
        }

        // ...
    }
}
Serj Sagan
  • 28,927
  • 17
  • 154
  • 183
Dan Bjorge
  • 1,514
  • 10
  • 10
  • 1
    +1 more for this response. I recently tried this by overriding the SendAsync by inheriting HttpClient however the override method doesn't handle all calls. (works for PostAsync but not GetAsync). This method with client handler works for all request methods! – samirahmed Feb 03 '14 at 18:08
  • 25
    Note that this solution does not work for *transient timeouts*. In such cases, it seems that cancellation is requested on the CancellationToken, which is likely to cause a TaskCanceledException to be thrown. – Gabriel Boya Sep 12 '14 at 13:48
  • @samirahmed look like it is a bug in HttpClient. I submitted it to [connect](http://connect.microsoft.com/VisualStudio/feedbackdetail/view/956324/httpclient-getasync-doesnt-call-virtual-sendasync) – Oleh Nechytailo Oct 03 '14 at 18:50
  • 5
    As @Gabi mentioned this won't work for timeouts. It seems `SendAsync` represents a *single* request operation, and so this is not the proper way to implement a retry mechanism. An external approach works better. – Ohad Schneider Oct 09 '14 at 09:46
  • 44
    I implemented this, and shot myself in the foot because, as mentioned, it doesn't work for timeouts. You shouldn't be like me and ignore the other commentors, you should listen to them. – Steve's a D Apr 27 '15 at 19:20
  • 4
    I've just tried this, and it's blowing up for me because the call to `base.SendAsync` is disposing the `HttpContent` passed to `client.PostAsync`. So, IME, your answer doesn't work. (The only reason I came to this answer was to avoid having to copy the content in order to retry! :-) ) – Grimm The Opiner Feb 28 '17 at 15:52
  • 2
    In .NET Core, `HttpClient` no longer unilaterally disposes of `HttpContent`, so that part of the problem goes away. See: https://github.com/dotnet/corefx/pull/19082/files – mountain traveller Apr 11 '18 at 21:38
  • The timeout problems around the `DelegatingHandler` approach can be solved. See [this comment](https://stackoverflow.com/questions/19260060/retrying-httpclient-unsuccessful-requests/48255857#comment86580847_35183487) to Muhammad Rehan Saeed's Polly approach. You can apply a Polly TimeoutPolicy to each retry. Polly TimeoutPolicy throws a custom `TimeoutRejectedException`, so this can be safely distinguished from cancellation by `CancellationToken`, whether that's external user cancellation or `HttpClient.Timeout` signalling overall timeout. – mountain traveller Apr 11 '18 at 21:46
  • You shouldn't dispose `HttpClient`. It will hog a socket until you quit the application. Rather persist the `HttpClient` as a global singleton. Ref: https://medium.com/@nuno.caneco/c-httpclient-should-not-be-disposed-or-should-it-45d2a8f568bc – Exegesis Aug 06 '21 at 20:27
79

ASP.NET Core 2.1 Answer

ASP.NET Core 2.1 added support for Polly directly. Here UnreliableEndpointCallerService is a class which accepts a HttpClient in its constructor. Failed requests will retry with an exponential back-off so that the next retry takes place in an exponentially longer time after the previous one:

services
    .AddHttpClient<UnreliableEndpointCallerService>()
    .AddTransientHttpErrorPolicy(
        x => x.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)));

Also, consider reading my blog post "Optimally Configuring HttpClientFactory".

Other Platforms Answer

This implementation uses Polly to retry with an exponential back-off so that the next retry takes place in an exponentially longer time after the previous one. It also retries if a HttpRequestException or TaskCanceledException is thrown due to a timeout. Polly is much easier to use than Topaz.

public class HttpRetryMessageHandler : DelegatingHandler
{
    public HttpRetryMessageHandler(HttpClientHandler handler) : base(handler) {}

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken) =>
        Policy
            .Handle<HttpRequestException>()
            .Or<TaskCanceledException>()
            .OrResult<HttpResponseMessage>(x => !x.IsSuccessStatusCode)
            .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)))
            .ExecuteAsync(() => base.SendAsync(request, cancellationToken));
}

using (var client = new HttpClient(new HttpRetryMessageHandler(new HttpClientHandler())))
{
    var result = await client.GetAsync("http://example.com");
}
Muhammad Rehan Saeed
  • 35,627
  • 39
  • 202
  • 311
  • 6
    Since July 2016 Polly can also natively handle a mixture of both exceptions and results (ie automatically treat certain result codes as failures). So, the handling of the StatusCode in the above example can now be expressed natively in a Polly Policy. The [Polly readme shows examples](https://github.com/App-vNext/Polly#handing-return-values-and-policytresult) – mountain traveller Sep 18 '16 at 21:25
  • I like the use of Polly, however (as pointed out in comments on the answers above), you should not rely on the DelegatingHandler approach if you want to catch timeouts, as the HttpClient will turn that into a TaskCancelledException. Best to create a wrapper client to decorate your HttpCLient with a retry implementation, which again could use Polly. – Thiago Silva Jul 19 '17 at 22:36
  • 1
    @ThiagoSilva Could you not just add a `.Handle() in the policy to handle cancellation? – Muhammad Rehan Saeed Jul 20 '17 at 09:10
  • @MuhammadRehanSaeed Looks nice! Is there a ! (a not) missing in `.OrResult(x => x.IsSuccessStatusCode)` ? – mountain traveller Jul 20 '17 at 13:11
  • @mountaintraveller Thanks, have fixed it. FYI, you can submit your own edits to answers. – Muhammad Rehan Saeed Jul 20 '17 at 13:19
  • For async tasks use "WaitAndRetryAsync" – Flappy Aug 16 '17 at 17:56
  • 1
    I would like to add that this solution suffers from the same problem as others here, on retry the content will be empty. – RVandersteen Sep 24 '17 at 14:15
  • 4
    Might be better to rename `retryCount` to `retryAttempt`. As the former implies the number of retries (which is always 5) and the latter actually refers to the current retry attempt, which goes from 1 to 5 in your example. – Alternatex Mar 14 '18 at 12:36
  • 1
  • 3
    To use the `DelegatingHandler` approach with timeouts, distinguish overall timeout (across all tries) from timeout-per-try. The Timeout property on HttpClient, `HttpClient.Timeout`, will act as an overall timeout across all retries combined. To impose a timeout-per-try, use a Polly TimeoutPolicy wrapped inside the WaitAndRetry policy. Further, Polly's TimeoutPolicy throws a `TimeoutRejectedException`. The WaitAndRetry policy should then handle `TimeoutRejectedException`, not `TaskCanceledException`. This distinguishes timeout-per-try from both external cancellation and all-retries-timeout. – mountain traveller Apr 11 '18 at 19:11
  • As a complement. With this setting, it is tried 5 times. After 3s, 9s, 27s, 81s and 243s makes a total of 363 seconds or approx. 6 minutes. – Markus Jun 11 '18 at 07:54
  • @MuhammadRehanSaeed Just learning about this, but I think this article explains why you can't just Handle: https://cm.engineering/transient-timeouts-and-the-retry-rabbit-hole-net-4-5-f406cebbf194. Looks like inside a DelegatingHandler you can't tell the difference in the cause of the cancellation (timeout vs caller canceled). In the case of caller canceled, you don't want to retry. The approach taken instead there is to nest handlers and use an independent cancellation source in the inner handler. – DennisWelu Sep 26 '18 at 21:01
  • 2
    @DennisWelu +1, Exactly. This is exactly why Polly's TimeoutPolicy throws a different exception (`TimeoutRejectedException`), so that a retry policy can distinguish that from user cancellation. The approach you suggest Dennis (independent cancellation source) is effectively what nesting Polly RetryPolicy with TimeoutPolicy does, as we suggest in the Polly + HttpClientFactory doco: https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory#use-case-applying-timeouts. – mountain traveller Nov 07 '18 at 18:05
  • 1
    This still does not handle a timeout on the HttpClient side, as this triggers the cancelation token, which on the next retry (and all following) will fail immediately since it checks the cancellation token in base.SendAsync – nrjohnstone Jul 02 '20 at 14:47
30

The current answers won't work as expected in all cases, specifically in the very common case of request timeout (see my comments there).

In addition, they implement a very naive retry strategy - many times you'd want something a bit more sophosticated, such as exponential backoff (which is the default in the Azure Storage Client API).

I stumbled upon TOPAZ while reading a related blog post (also offering the misguided internal retry approach). Here's what I came up with:

// sample usage: var response = await RequestAsync(() => httpClient.GetAsync(url));
Task<HttpResponseMessage> RequestAsync(Func<Task<HttpResponseMessage>> requester)
{
    var retryPolicy = new RetryPolicy(transientErrorDetectionStrategy, retryStrategy);
    //you can subscribe to the RetryPolicy.Retrying event here to be notified 
    //of retry attempts (e.g. for logging purposes)
    return retryPolicy.ExecuteAsync(async () =>
    {
        HttpResponseMessage response;
        try
        {
            response = await requester().ConfigureAwait(false);
        }
        catch (TaskCanceledException e) //HttpClient throws this on timeout
        {
            //we need to convert it to a different exception
            //otherwise ExecuteAsync will think we requested cancellation
            throw new HttpRequestException("Request timed out", e);
        }
        //assuming you treat an unsuccessful status code as an error
        //otherwise just return the respone here
        return response.EnsureSuccessStatusCode(); 
    });
}

Note the requester delegate parameter. It should not be an HttpRequestMessage since you can't send the same request multiple times. As for the strategies, that depends on your use case. For example, a transient error detection strategy could be as simple as:

private sealed class TransientErrorCatchAllStrategy : ITransientErrorDetectionStrategy
{
    public bool IsTransient(Exception ex)
    {
        return true;
    }
}

As for the retry strategy, TOPAZ offers three options:

  1. FixedInterval
  2. Incremental
  3. ExponentialBackoff

For example, here's the TOPAZ equivalent of what the Azure Client Storage Library uses for default:

int retries = 3;
var minBackoff = TimeSpan.FromSeconds(3.0);
var maxBackoff = TimeSpan.FromSeconds(120.0);
var deltaBackoff= TimeSpan.FromSeconds(4.0);
var strategy = new ExponentialBackoff(retries, minBackoff, maxBackoff, deltaBackoff);

For more information see http://msdn.microsoft.com/en-us/library/hh680901(v=pandp.50).aspx

EDIT Note that if your request contains an HttpContent object, you'll have to regenerate it every time as that will be disposed by HttpClient as well (thanks for catching that Alexandre Pepin). For example () => httpClient.PostAsync(url, new StringContent("foo"))).

Ohad Schneider
  • 36,600
  • 15
  • 168
  • 198
  • 15
    Or check out [Polly](https://github.com/michael-wolfenden/Polly). A more lightweight and IMHO a much cleaner (vs over-engineered) library to do the same! – fretje Jul 23 '15 at 14:38
  • 1
    "you can't send the same request multiple times". Can you expand on that? I have a delegating handler that does just that and it *seems* to work just fine. Am I missing something? – bornfromanegg Dec 08 '15 at 17:39
  • @bornfromanegg when I tried passing an `HttpRequestMessage` instead of a delegate as I do now (which typically looks like `() => client.GetAsync(url)`) then the first try would work, but the following retries would fail, throwing an exception about not being able to reuse the same request message (I can't recall the exact exception type and wording) – Ohad Schneider Dec 09 '15 at 16:45
  • blog is down currently, here is the link to the webarchive https://web.archive.org/web/20150117043128/http://blog.devscrum.net/2014/05/building-a-transient-retry-handler-for-the-net-httpclient/ – Nicramus Jan 15 '20 at 13:17
19

Duplicating the StringContent isn't probably the best idea. But simple modification could fix the problem. Just modify the function and create the StringContent object inside of the loop, something like:

public HttpResponseMessage ExecuteWithRetry(string url, string contentString)
{
   HttpResponseMessage result = null;
   bool success = false;
   using (var client = new HttpClient())
   {
      do
      {
         result = client.PostAsync(url, new StringContent(contentString)).Result;
         success = result.IsSuccessStatusCode;
      }
      while (!success);
  }    

  return result;
} 

and then call it

ExecuteWithRetry("http://www.requestb.in/xfxcva" /*valid url*/, "Hello World");
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
VladL
  • 12,769
  • 10
  • 63
  • 83
  • This works but is really inconvenient for non string httpcontent types. Like mutlipartforms etc. – samirahmed Oct 21 '13 at 21:35
  • agree, I would also love to suppress the disposing of content. But you would need to construct the content object somewhere, so why not inside of the given function? :) – VladL Oct 22 '13 at 07:58
  • the creation of the httpcontent object is managed by another component on my project and at times its non trivial and the request issuing logic is intended to be as generic and reusable as possible. It kind of defeats the point if the content has be created everytime. I work around this by essentially copying the httpcontext myself. – samirahmed Oct 23 '13 at 20:30
  • This will only retry requests that returned with some status code from the server. Many times that won't be the case and an exception will be thrown, for example on timeouts. You have the right idea though with the external wrapper. – Ohad Schneider Oct 09 '14 at 09:47
  • 3
    @VladL you should not be disposing and recreating the HttpClient in a loop. The HttpClient (though it implements IDisposable) is meant to be reused for the lifecycle of the application, otherwise it will leave a bunch of sockets open. If you run under load, you will end up getting SocketExceptions, which means you've run out of sockets. – Thiago Silva Jul 19 '17 at 22:32
7

This is what I achieved using polly.

nuget

https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly

https://www.nuget.org/packages/Polly

using Polly;
using Polly.Extensions.Http;

//// inside configure service
services.AddHttpClient("RetryHttpClient", c =>
{
    c.BaseAddress = new Uri($"{configuration["ExternalApis:MyApi"]}/");
    c.DefaultRequestHeaders.Add("Accept", "application/json");
    c.Timeout = TimeSpan.FromMinutes(5);
    c.DefaultRequestHeaders.ConnectionClose = true;

}).AddPolicyHandler(GetRetryPolicy());

//// add this method to give retry policy
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        //// 408,5xx
        .HandleTransientHttpError()
        //// 404
        .OrResult(msg => msg.StatusCode == HttpStatusCode.NotFound)
        //// 401
        .OrResult(msg => msg.StatusCode == HttpStatusCode.Unauthorized)
        //// Retry 3 times, with wait 1,2 and 4 seconds.
        .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
cdev
  • 5,043
  • 2
  • 33
  • 32
6

This builds off the accepted answer but adds the ability to pass in the amount of retries, plus the ability to add non-blocking delays / wait time to each request. It also uses a try catch to ensure the retry continues to happen after an exception has occurred. And last, I added code to break out of the loop in the case of BadRequests, you don't want to resend the same bad request multiple times.

public class HttpRetryHandler : DelegatingHandler
{
    private int MaxRetries;
    private int WaitTime;

    public HttpRetryHandler(HttpMessageHandler innerHandler, int maxRetries = 3, int waitSeconds = 0)
        : base(innerHandler)
    {
        MaxRetries = maxRetries;
        WaitTime = waitSeconds * 1000; 
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
            HttpResponseMessage response = null;

            for (int i = 1; i <= MaxRetries; i++)
            {           
                try
                {
                    response = await base.SendAsync(request, cancellationToken);
                    if (response.IsSuccessStatusCode)
                    {
                        return response;
                    }
                    else if(response.StatusCode == HttpStatusCode.BadRequest)
                    {
                        // Don't reattempt a bad request
                        break; 
                    }
                    else
                    {
                        _log.WarnFormat("Attempt {0} -> HTTP Status Code = {1}", i, response.StatusCode);
                    }
                }
                catch(Exception ex)
                {

                    _log.WarnFormat("Attempt {0} -> Exception = {1}", i, ex);
                }

                if(WaitTime > 0)
                {
                    await Task.Delay(WaitTime);
                }
            }

        return response;
    }
}
TroySteven
  • 4,885
  • 4
  • 32
  • 50
4

With RestEase And Task, on retry with httpClient reused in many call (singleton) it frezze and throw TaskCanceledException. To fix this whe need to Dispose() the failed response before retry

public class RetryHandler : DelegatingHandler
{
    // Strongly consider limiting the number of retries - "retry forever" is
    // probably not the most user friendly way you could respond to "the
    // network cable got pulled out."
    private const int MaxRetries = 3;

    public RetryHandler(HttpMessageHandler innerHandler)
        : base(innerHandler)
    { }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        HttpResponseMessage response = null;
        for (int i = 0; i < MaxRetries; i++)
        {
            response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
            if (response.IsSuccessStatusCode) {
                return response;
            }

            response.Dispose();
        }

        return response;
    }
}
1

You also refer to Building a Transient Retry Handler for the .NET HttpClient. Visit refer to KARTHIKEYAN VIJAYAKUMAR post.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.SqlClient;
using System.Net.Http;
using System.Threading;
using System.Diagnostics;
using System.Net;
using Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling;

namespace HttpClientRetyDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var url = "http://RestfulUrl";
            var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);

            var handler = new RetryDelegatingHandler
            {
                UseDefaultCredentials = true,
                PreAuthenticate = true,
                Proxy = null
            };

            HttpClient client = new HttpClient(handler);
            var result = client.SendAsync(httpRequestMessage).Result.Content
                .ReadAsStringAsync().Result;

            Console.WriteLine(result.ToString());
            Console.ReadKey();

        }
    }

    /// <summary>
    /// Retry Policy = Error Detection Strategy + Retry Strategy
    /// </summary>
    public static class CustomRetryPolicy
    {
        public static RetryPolicy MakeHttpRetryPolicy()
        {
            // The transient fault application block provides three retry policies
            //  that you can use. These are:
            return new RetryPolicy(strategy, exponentialBackoff);
        }
    }

    /// <summary>
    /// This class is responsible for deciding whether the response was an intermittent
    /// transient error or not.
    /// </summary>
    public class HttpTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy
    {
        public bool IsTransient(Exception ex)
        {
            if (ex != null)
            {
                HttpRequestExceptionWithStatus httpException;
                if ((httpException = ex as HttpRequestExceptionWithStatus) != null)
                {
                    if (httpException.StatusCode == HttpStatusCode.ServiceUnavailable)
                    {
                        return true;
                    }
                    else if (httpException.StatusCode == HttpStatusCode.MethodNotAllowed)
                    {
                        return true;
                    }
                    return false;
                }
            }
            return false;
        }
    }

    /// <summary>
    /// The retry handler logic is implementing within a Delegating Handler. This has a
    /// number of advantages.
    /// An instance of the HttpClient can be initialized with a delegating handler making
    /// it super easy to add into the request pipeline.
    /// It also allows you to apply your own custom logic before the HttpClient sends the
    /// request, and after it receives the response.
    /// Therefore it provides a perfect mechanism to wrap requests made by the HttpClient
    /// with our own custom retry logic.
    /// </summary>
    class RetryDelegatingHandler : HttpClientHandler
    {
        public RetryPolicy retryPolicy { get; set; }
        public RetryDelegatingHandler()
            : base()
        {
            retryPolicy = CustomRetryPolicy.MakeHttpRetryPolicy();
        }


        protected async override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            HttpResponseMessage responseMessage = null;
            var currentRetryCount = 0;
            //On Retry => increments the retry count
            retryPolicy.Retrying += (sender, args) =>
            {
                currentRetryCount = args.CurrentRetryCount;
            };
            try
            {
                await retryPolicy.ExecuteAsync(async () =>
                {
                    responseMessage = await base.SendAsync(request, cancellationToken)
                        .ConfigureAwait(false);
                    if ((int)responseMessage.StatusCode > 500)
                    {
                        // When it fails after the retries, it would throw the exception
                        throw new HttpRequestExceptionWithStatus(
                            string.Format("Response status code {0} indicates server error",
                                (int)responseMessage.StatusCode))
                        {
                            StatusCode = responseMessage.StatusCode,
                            CurrentRetryCount = currentRetryCount
                        };
                    }// returns the response to the main method(from the anonymous method)
                    return responseMessage;
                }, cancellationToken).ConfigureAwait(false);
                return responseMessage;// returns from the main method => SendAsync
            }
            catch (HttpRequestExceptionWithStatus exception)
            {
                if (exception.CurrentRetryCount >= 3)
                {
                    //write to log
                }
                if (responseMessage != null)
                {
                    return responseMessage;
                }
                throw;
            }
            catch (Exception)
            {
                if (responseMessage != null)
                {
                    return responseMessage;
                }
                throw;
            }
        }
    }

    /// <summary>
    /// Custom HttpRequestException to allow include additional properties on my exception,
    /// which can be used to help determine whether the exception is a transient
    /// error or not.
    /// </summary>
    public class HttpRequestExceptionWithStatus : HttpRequestException
    {
        public HttpStatusCode StatusCode { get; set; }
        public int CurrentRetryCount { get; set; }

        public HttpRequestExceptionWithStatus()
            : base() { }

        public HttpRequestExceptionWithStatus(string message)
            : base(message) { }

        public HttpRequestExceptionWithStatus(string message, Exception inner)
            : base(message, inner) { }
    }
}
Pierre-Loup Pagniez
  • 3,611
  • 3
  • 29
  • 29
Colin Chen
  • 404
  • 4
  • 10
1

I tried it and worked while using unit and integration tests. However, it stuck when I actually called from REST URL. I found this interesting post which explains why it gets stuck at this line.

response = await base.SendAsync(request, cancellationToken);

The fix to this is that you have .ConfigureAwait(false) added at the end.

response = await base.SendAsync(request, token).ConfigureAwait(false);

I also added create linked token part there like this.

var linkedToken = cancellationToken.CreateLinkedSource();
linkedToken.CancelAfter(new TimeSpan(0, 0, 5, 0));
var token = linkedToken.Token;

HttpResponseMessage response = null;
for (int i = 0; i < MaxRetries; i++)
{
    response = await base.SendAsync(request, token).ConfigureAwait(false);
    if (response.IsSuccessStatusCode)
    {
        return response;
    }
}

return response;
ByteHamster
  • 4,884
  • 9
  • 38
  • 53
0

i have almost the same issue. HttpWebRequest queueing library, which guarantees request delivery I just updated (see EDIT3) my approach to avoid crashes, but i still need general mechanism to guarantee message delivery (or re-delivery in case message was not delivered).

Community
  • 1
  • 1
Vitalii Vasylenko
  • 4,776
  • 5
  • 40
  • 64
0

I have the same problem and solved . it's about the "StringContent" / "HttpContent"

Please check Amogh Natu's Blog which help me to solve this problem

The problem with this code is, when the first call to PostAsync is made and it fails, the httpContent object is disposed. This is as designed in the HttpClient class. Refer the comment in this method. Although this seems odd, they intent to do this so that the user doesn’t have to do this explicitly and also to avoid the same request being Posted more than once.

So what happens is, when the first call fails, httpContent is disposed, then as we have retry mechanism, it tries to make the post call again, now with a disposed object and hence this time, the call fails with an ObjectDisposedException.

An easy way to resolve this is to NOT use a variable to store httpContent and instead, create http content directly while making the call. Something like this.

http://amoghnatu.net/2017/01/12/cannot-access-a-disposed-object-system-net-http-stringcontent-while-having-retry-logic/

0

Adding an answer that uses both Polly + Retry policy + per-retry Timeout policy, as top answer does not address that:

Policy
    .Handle<HttpRequestException>()
    .Or<TaskCanceledException>()
    .Or<TimeoutRejectedException>()
    .OrResult<HttpResponseMessage>(x => !x.IsSuccessStatusCode)
    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)))
    .WrapAsync(
        Policy.TimeoutAsync(TimeSpan.FromSeconds(1), delegate (Context ctx, TimeSpan timeSpan, Task task)
        {
            // Do some on-timeout action 
            return Task.CompletedTask;
        })
    )
    .ExecuteAsync(() =>
    {
        return httpclient.PostAsync(url, httpRequest);
    });
aybassiouny
  • 559
  • 5
  • 14
-3
        //Could retry say 5 times          
        HttpResponseMessage response;
        int numberOfRetry = 0;
        using (var httpClient = new HttpClient())
        {
            do
            {
                response = await httpClient.PostAsync(uri, content);
                numberOfRetry++;
            } while (response.IsSuccessStatusCode == false | numberOfRetry < 5);
        }
return response;



        .........
  • Its always better to handle specific exceptions rather doing something like that, in the end it will cause more problems – CDrosos Sep 06 '20 at 14:24