重试HttpClient不成功的请求

sam*_*med 40 c# httpcontent dotnet-httpclient

我正在构建一个给出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"));
Run Code Online (Sandbox Code Playgroud)

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

它产生了这个例外

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)
Run Code Online (Sandbox Code Playgroud)

反正有没有复制HttpContent对象或重用它?

Dan*_*rge 70

不考虑实现包装的重试功能,而是HttpClient考虑HttpClient使用在HttpMessageHandler内部执行重试逻辑的方法来构造.例如:

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

        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我实施了这个,然后用脚射击自己,因为如上所述,它不适用于超时.你不应该像我一样忽略其他评论家,你应该听他们说. (24认同)
  • 请注意,此解决方案不适用于*瞬态超时*.在这种情况下,似乎在CancellationToken上请求取消,这很可能导致抛出TaskCanceledException. (17认同)
  • 正如@Gabi所提到的,这不适用于超时.似乎"SendAsync"表示*single*请求操作,因此这不是实现重试机制的正确方法.外部方法效果更好. (3认同)
  • 我刚试过这个,而且它正在为我吹嘘,因为对`base.SendAsync`的调用正在处理传递给`client.PostAsync`的`HttpContent`.所以,IME,你的答案不起作用.(我得出这个答案的唯一原因是为了避免复制内容以便重试!:-)) (3认同)
  • 在 .NET Core 中,`HttpClient` 不再单方面处理 `HttpContent`,从而部分问题消失了。请参阅:https://github.com/dotnet/corefx/pull/19082/files (2认同)

Muh*_*eed 46

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)));
Run Code Online (Sandbox Code Playgroud)

另外,请考虑阅读我的博客文章"Optimally Configure HttpClientFactory".

其他平台答案

此实现使用Polly以指数退避重试,以便下一次重试发生在前一次重试之后的指数级更长的时间内.如果它也重试HttpRequestExceptionTaskCanceledException因超时异常.Polly比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");
}
Run Code Online (Sandbox Code Playgroud)

  • 自2016年7月起,Polly还可以原生地处理异常和结果的混合(即自动将某些结果代码视为失败).因此,上面示例中StatusCode的处理现在可以在Polly Policy中本地表示.[Polly自述文件显示示例](https://github.com/App-vNext/Polly#handing-return-values-and-policytresult) (5认同)
  • 可能最好将`retryCount`重命名为`retryAttempt`.前者表示重试次数(总是为5),后者实际上是指当前的重试尝试,在您的示例中从1到5. (2认同)
  • 要将`DelegatingHandler` 方法与超时一起使用,请将总体超时(跨所有尝试)与每次尝试超时区分。HttpClient 上的 Timeout 属性 `HttpClient.Timeout` 将作为所有重试组合的整体超时。要强制每次尝试超时,请使用包装在 WaitAndRetry 策略中的 Polly TimeoutPolicy。此外,Polly 的 TimeoutPolicy 会抛出一个 `TimeoutRejectedException`。然后,WaitAndRetry 策略应该处理“TimeoutRejectedException”,而不是“TaskCanceledException”。这将每次尝试超时与外部取消和所有重试超时区分开来。 (2认同)
  • @DennisWelu +1,没错。这正是 Polly 的 TimeoutPolicy 抛出不同异常 (`TimeoutRejectedException`) 的原因,以便重试策略可以将其与用户取消区分开来。您建议 Dennis(独立取消源)的方法实际上是将 Polly RetryPolicy 与 TimeoutPolicy 嵌套的方法,正如我们在 Polly + HttpClientFactory 文档中所建议的:https://github.com/App-vNext/Polly/wiki/Polly-and- HttpClientFactory#use-case-applying-timeouts。 (2认同)

Oha*_*der 28

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(); 
    });
}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)

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

  • 或者查看[Polly](https://github.com/michael-wolfenden/Polly).一个更轻量级的恕我直言,更清洁(vs过度设计)库也可以做到这一点! (13认同)

Vla*_*adL 15

复制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;
} 
Run Code Online (Sandbox Code Playgroud)

然后打电话给它

ExecuteWithRetry("http://www.requestb.in/xfxcva" /*valid url*/, "Hello World");
Run Code Online (Sandbox Code Playgroud)

  • @VladL,您不应该在循环中处理和重新创建 HttpClient。HttpClient(尽管它实现了 IDisposable)旨在在应用程序的生命周期中重用,否则它将留下一堆打开的套接字。如果你在负载下运行,你最终会得到 SocketExceptions,这意味着你已经用完了套接字。 (3认同)

cde*_*dev 7

这就是我使用 polly 实现的目标。

努盖特

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)));
}
Run Code Online (Sandbox Code Playgroud)


Tro*_*ven 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

}