Повторная попытка 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"));

(Очевидно, я не пытаюсь бесконечно, но приведенный выше код по сути то, что я хочу).

Это дает это исключение

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 или использовать его повторно?

13 ответов

Решение

Вместо реализации функции повтора, которая переносит 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;
        }

        // ...
    }
}

ASP.NET Core 2.1 Ответ

В ASP.NET Core 2.1 добавлена ​​поддержка Полли напрямую. Вот UnreliableEndpointCallerService это класс, который принимает HttpClient в своем конструкторе. Неудачные запросы будут повторяться с экспоненциальной задержкой, поэтому следующая повторная попытка будет происходить через экспоненциально более длительное время после предыдущей:

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

Также рассмотрите возможность прочтения моего блога "Оптимальная настройка HttpClientFactory".

Ответ других платформ

Эта реализация использует Polly для повторной попытки с экспоненциальным откатом, так что следующая повторная попытка будет происходить в течение экспоненциально более длительного времени после предыдущей. Это также повторяет, если HttpRequestException или же TaskCanceledException выбрасывается из-за тайм-аута. Полли намного проще в использовании, чем Топаз.

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

Текущие ответы не будут работать должным образом во всех случаях, особенно в очень распространенном случае тайм-аута запроса (см. Мои комментарии там).

Кроме того, они реализуют очень наивную стратегию повторных попыток - во многих случаях вам нужно что-то более сложное, например, экспоненциальный откат (который используется по умолчанию в клиентском API хранилища Azure).

Я наткнулся на 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;
    }
}

Что касается стратегии повтора, ТОПАЗ предлагает три варианта:

  1. FixedInterval
  2. дополнительный
  3. экспоненциальная выдержка

Например, вот эквивалент TOPAZ того, что по умолчанию клиентская библиотека хранилища Azure использует:

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 также (спасибо, что поймали этого Александра Пепина). Например () => httpClient.PostAsync(url, new StringContent("foo"))),

Дублирование 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");

Это то, чего я добился, используя Polly.

самородок

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

https://www.nuget.org/packages/Полли

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

Это основывается на принятом ответе, но добавляет возможность передавать количество повторных попыток, а также добавляет возможность добавлять неблокирующие задержки / время ожидания к каждому запросу. Он также использует ловушку попытки, чтобы повторная попытка продолжалась после возникновения исключения. И наконец, я добавил код для выхода из цикла в случае 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;
    }
}

}

С RestEase And Task, при повторной попытке с использованием httpClient, повторно используемого во многих вызовах (singleton), он блокируется и выдает 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;
    }
}

Вы также обратитесь к разделу Создание обработчика временных попыток для посещения.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();
                    handler.UseDefaultCredentials = true;
                    handler.PreAuthenticate = true;
                    handler.Proxy = null;
                    HttpClient client = new HttpClient(handler);
                    var result = client.SendAsync(httpRequestMessage).Result.Content.ReadAsStringAsync().Result;
                    Console.WriteLine(result.ToString());
                    Console.ReadKey();

                }
                //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.
                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;
                    }
                    }
                }
                //Retry Policy = Error Detection Strategy + Retry Strategy
                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);
                    }
                }
                //This class is responsible for deciding whether the response was an intermittent transient error or not.
                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;
                    }
                }
                //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.
                public class HttpRequestExceptionWithStatus : HttpRequestException
                {
                    public HttpRequestExceptionWithStatus() : base() { }
                    public HttpRequestExceptionWithStatus(string message) : base(message) { }
                    public HttpRequestExceptionWithStatus(string message, Exception inner) : base(message, inner) { }
                    public HttpStatusCode StatusCode { get; set; }
                    public int CurrentRetryCount { get; set; }
                }
                }
            }

Я попробовал это и работал, используя юнит и интеграционные тесты. Тем не менее, он застрял, когда я на самом деле позвонил с 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;

Добавление ответа, который использует политику Polly + Retry + политику тайм-аута для повторной попытки, так как верхний ответ не касается этого:

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

У меня почти такая же проблема. Библиотека очередей HttpWebRequest, которая гарантирует доставку запросов Я только что обновил (см. РЕДАКТИРОВАТЬ 3) мой подход, чтобы избежать сбоев, но мне все еще нужен общий механизм для гарантии доставки сообщений (или повторной доставки в случае, если сообщение не было доставлено).

У меня такая же проблема и решена. это о "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/

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



        .........
Другие вопросы по тегам