64

我正在构建一个给定 HttpContent 对象的函数,它将发出请求并在失败时重试。但是,我收到异常说 HttpContent 对象在发出请求后被释放。无论如何要复制或复制 HttpContent 对象,以便我可以发出多个请求。

 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"));

(显然我不会无限期地尝试,但上面的代码基本上是我想要的)。

它产生这个异常

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)

无论如何要复制 HttpContent 对象或重用它吗?

4

13 回答 13

87

与其实现包装 的重试功能,不如HttpClient考虑HttpClient用在HttpMessageHandler内部执行重试逻辑的 a 构造 。例如:

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;
        }

        // ...
    }
}
于 2013-10-29T04:59:24.017 回答
68

ASP.NET Core 2.1 答案

ASP.NET Core 2.1 直接添加了Polly的支持。这UnreliableEndpointCallerService是一个HttpClient在其构造函数中接受 a 的类。失败的请求将以指数回退重试,以便下一次重试发生在前一次之后的指数更长的时间内:

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

另外,请考虑阅读我的博文“优化配置 HttpClientFactory”

其他平台回答

此实现使用Polly以指数回退重试,以便下一次重试在前一次重试之后的指数更长的时间内进行。如果由于超时而抛出HttpRequestExceptionor ,它也会重试。TaskCanceledExceptionPolly 比 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");
}
于 2016-02-03T17:13:49.480 回答
30

当前的答案在所有情况下都不会按预期工作,特别是在请求超时的非常常见的情况下(请参阅我的评论)。

此外,它们实施了一种非常幼稚的重试策略 - 很多时候您想要一些更复杂的东西,例如指数退避(这是 Azure 存储客户端 API 中的默认设置)。

我在阅读相关博客文章时偶然发现了TOPAZ(也提供了错误的内部重试方法)。这是我想出的:

// 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(); 
    });
}

注意requester委托参数。它应该是HttpRequestMessage因为您不能多次发送相同的请求。至于策略,这取决于您的用例。例如,瞬态错误检测策略可能很简单:

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

至于重试策略,TOPAZ 提供了三种选择:

  1. 固定间隔
  2. 增加的
  3. 指数退避

例如,下面是 Azure 客户端存储库默认使用的 TOPAZ:

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);

有关详细信息,请参阅http://msdn.microsoft.com/en-us/library/hh680901(v=pandp.50).aspx

编辑请注意,如果您的请求包含一个HttpContent对象,则每次都必须重新生成它,因为它也会被处理HttpClient(感谢您抓住 Alexandre Pepin)。例如() => httpClient.PostAsync(url, new StringContent("foo"))).

于 2014-10-09T12:29:13.317 回答
18

复制 StringContent 可能不是最好的主意。但简单的修改可以解决问题。只需修改函数并在循环内创建 StringContent 对象,例如:

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;
} 

然后调用它

ExecuteWithRetry("http://www.requestb.in/xfxcva" /*valid url*/, "Hello World");
于 2013-10-21T09:42:25.030 回答
5

这建立了已接受的答案,但增加了传递重试次数的能力,并增加了向每个请求添加非阻塞延迟/等待时间的能力。它还使用 try catch 来确保在发生异常后继续重试。最后,我添加了代码以在 BadRequests 的情况下跳出循环,您不想多次重新发送相同的错误请求。

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 = 0; 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; 
                }
            }
            catch
            {
                // Ignore Error As We Will Attempt Again
            }
            finally
            {
                response.Dispose(); 
            }

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

        return response;
    }
}

}

于 2019-07-17T19:33:14.290 回答
4

这是我使用 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)));
}
于 2021-01-06T08:51:18.670 回答
4

使用 RestEase 和 Task,在多次调用(单例)中重用 httpClient 重试时,它会冻结并抛出 TaskCanceledException。要解决此问题,需要在重试之前 Dispose() 失败的响应

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;
    }
}
于 2018-10-30T15:04:07.190 回答
1

您还可以参考为 .NET HttpClient 构建瞬态重试处理程序。访问请参阅KARTHIKEYAN VIJAYAKUMAR帖子。

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) { }
    }
}
于 2015-12-15T05:47:05.070 回答
1

我在使用单元和集成测试时尝试过并工作过。但是,当我实际从 REST URL 调用时,它卡住了。我发现这篇有趣的帖子解释了为什么它会卡在这条线上。

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

对此的解决方法是您.ConfigureAwait(false)在最后添加了。

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

我还像这样在那里添加了创建链接令牌部分。

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;
于 2016-05-05T13:38:56.743 回答
0

我有几乎同样的问题。 HttpWebRequest 队列库,它保证了请求的传递 我刚刚更新了(参见 EDIT3)我避免崩溃的方法,但我仍然需要通用机制来保证消息传递(或在消息未传递的情况下重新传递)。

于 2013-10-17T13:23:39.163 回答
0

我有同样的问题并解决了。这是关于“StringContent”/“HttpContent”的

请查看 Amogh Natu 的博客,它可以帮助我解决这个问题

这段代码的问题是,当第一次调用 PostAsync 失败时,会释放 httpContent 对象。这是在 HttpClient 类中设计的。请参阅此方法中的注释。尽管这看起来很奇怪,但他们打算这样做,以便用户不必明确地执行此操作,并且还避免多次发布相同的请求。

所以发生的情况是,当第一次调用失败时,httpContent 被释放,然后由于我们有重试机制,它尝试再次进行 post 调用,现在使用释放的对象,因此这次调用失败并出现 ObjectDisposedException。

解决此问题的一种简单方法是不使用变量来存储 httpContent,而是在调用时直接创建 http 内容。像这样的东西。

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

于 2020-12-02T14:38:00.960 回答
0

添加一个同时使用 Polly + 重试策略 + 每次重试超时策略的答案,因为最佳答案没有解决这个问题:

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);
    });
于 2022-03-06T00:09:58.403 回答
-2
        //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;



        .........
于 2018-01-15T01:21:54.783 回答