C# 重试 HttpClient 不成功的请求

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/19260060/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-10 14:31:53  来源:igfitidea点击:

Retrying HttpClient Unsuccessful Requests

c#dotnet-httpclienthttpcontent

提问by samirahmed

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.

我正在构建一个给定 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"));

(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?

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

采纳答案by Dan Bjorge

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

与其实现包装 的重试功能,不如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;
        }

        // ...
    }
}

回答by Vitalii Vasylenko

i have almost the same issue. HttpWebRequest queueing library, which guarantees request deliveryI 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).

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

回答by VladL

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:

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

and then call it

然后调用它

ExecuteWithRetry("http://www.requestb.in/xfxcva" /*valid url*/, "Hello World");

回答by Ohad Schneider

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

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

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

我在阅读相关博客文章时偶然发现了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(); 
    });
}

Note the requesterdelegate parameter. It should notbe an HttpRequestMessagesince 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:

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

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

As for the retry strategy, TOPAZ offers three options:

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

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

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

例如,下面是 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);

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

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

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

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

回答by Colin Chen

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

您还可以参考为 .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) { }
    }
}

回答by Muhammad Rehan Saeed

ASP.NET Core 2.1 Answer

ASP.NET Core 2.1 答案

ASP.NET Core 2.1 added supportfor Pollydirectly. Here UnreliableEndpointCallerServiceis a class which accepts a HttpClientin 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:

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

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

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

另外,请考虑阅读我的博客文章“最佳配置 HttpClientFactory”

Other Platforms Answer

其他平台回答

This implementation uses Pollyto 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 HttpRequestExceptionor TaskCanceledExceptionis thrown due to a timeout. Polly is much easier to use than Topaz.

此实现使用Polly以指数退避方式重试,以便下一次重试在前一次重试之后的指数更长时间内进行。如果 a HttpRequestExceptionorTaskCanceledException由于超时而抛出,它也会重试。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");
}

回答by Pranav Patel

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 postwhich explains why it gets stuck at this line.

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

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

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

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

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;

回答by Kwame Gyau-Annoaning

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



        .........

回答by Pascal Martin

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

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

回答by TroySteven

This builds off the accepted answer but adds the ability to pass in the amount of retries, plus adds 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.

这建立在接受的答案的基础上,但增加了传递重试次数的能力,并增加了为每个请求添加非阻塞延迟/等待时间的能力。它还使用 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;
    }
}

}

}