Ошибка получения сообщения "Запрос уже отправлен" при использовании Polly

В настоящее время я использую Polly для ограничения количества отправляемых запросов. Это политика, которую я в настоящее время имею:

private AsyncPolicyWrap<HttpResponseMessage> DefineAndRetrieveResiliencyStrategy()
{
    HttpStatusCode[] retryCodes = {
       HttpStatusCode.InternalServerError,
       HttpStatusCode.BadGateway,
       HttpStatusCode.GatewayTimeout
    };

    var waitAndRetryPolicy = Policy
        .HandleResult<HttpResponseMessage>(e => e.StatusCode == HttpStatusCode.ServiceUnavailable || e.StatusCode == (HttpStatusCode)429)
        .WaitAndRetryAsync(10,
            attempt => TimeSpan.FromSeconds(5), (exception, calculatedWaitDuration) =>
            {
                _log.Info($"Bitfinex API server is throttling our requests. Automatically delaying for {calculatedWaitDuration.TotalMilliseconds}ms");
            }
        );

    var circuitBreakerPolicyForRecoverable = Policy
        .Handle<HttpResponseException>()
        .OrResult<HttpResponseMessage>(r => retryCodes.Contains(r.StatusCode))
        .CircuitBreakerAsync(
            handledEventsAllowedBeforeBreaking: 3,
            durationOfBreak: TimeSpan.FromSeconds(3),
            onBreak: (outcome, breakDelay) =>
            {
                _log.Info($"Polly Circuit Breaker logging: Breaking the circuit for {breakDelay.TotalMilliseconds}ms due to: {outcome.Exception?.Message ?? outcome.Result.StatusCode.ToString()}");

            },
            onReset: () => _log.Info("Polly Circuit Breaker logging: Call ok... closed the circuit again"),
            onHalfOpen: () => _log.Info("Polly Circuit Breaker logging: Half-open: Next call is a trial")
        );

    return Policy.WrapAsync(waitAndRetryPolicy, circuitBreakerPolicyForRecoverable);
}

У меня есть следующий отправитель запроса:

private async Task<string> SendRequest(GenericRequest request, string httpMethod, string publicKey, string privateKey)
{
    var resiliencyStrategy = DefineAndRetrieveResiliencyStrategy();

    using (var client = new HttpClient())
    using (var httpRequest = new HttpRequestMessage(new HttpMethod(httpMethod), request.request))
    {
        string json = JsonConvert.SerializeObject(request);
        string json64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
        byte[] data = Encoding.UTF8.GetBytes(json64);

        client.BaseAddress = new Uri(Properties.Settings.Default.BitfinexUri);

        var hashMaker = new HMACSHA384(Encoding.UTF8.GetBytes(privateKey));
        byte[] hash = hashMaker.ComputeHash(data);
        string signature = GetHexString(hash);

        httpRequest.Headers.Add("X-BFX-APIKEY", publicKey);
        httpRequest.Headers.Add("X-BFX-PAYLOAD", json64);
        httpRequest.Headers.Add("X-BFX-SIGNATURE", signature);

        var message = await resiliencyStrategy.ExecuteAsync(() => client.SendAsync(httpRequest));
        var response = message.Content.ReadAsStringAsync().Result;

        return response;
    }
}

Как только код попадает в waitAndRetryPolicy и ожидает необходимое количество времени, я получаю следующую ошибку:

System.InvalidOperationException: 'Сообщение с запросом уже отправлено. Невозможно отправить одно и то же сообщение с запросом несколько раз.'

Я понимаю, что это происходит, потому что я снова отправляю тот же запрос HttpRequest, но разве библиотека Polly не должна решать такую ​​проблему?

3 ответа

Это исключение:

System.InvalidOperationException: 'Сообщение с запросом уже отправлено. Невозможно отправить одно и то же сообщение с запросом несколько раз.'

брошен внутренними HttpClient если вы вызываете напрямую в любую перегрузку.SendAsync(...) с HttpRequestMessage, которая уже была отправлена.

Если вы используете.NET Core, рекомендуется использовать Polly с HttpClientFactory: это устраняет указанное выше исключение, выполняя политику (например, повтор) через DelegatingHandler в HttpClient, Это также решает проблему истощения сокетов, которая может быть вызвана частым созданием / удалением HttpClient, к которому может быть уязвим код, размещенный в вопросе.

Если вы используете.NET Framework, рекомендуемые решения:

  • повторить способ, которым HttpClientFactory размещает политику в DelegatingHandler; или же
  • рефакторинг вашего кода для производства нового экземпляра HttpRequestMessage (или клонировать существующий экземпляр) в коде, выполняемом с помощью политики.

В этом вопросе о стекопереработке подробно обсуждается проблема и множество вариантов описанных выше решений.

Если бы вы разделили свой код на следующие две функции:

      private HttpRequestMessage CreateRequest(GenericRequest request, string httpMethod, string publicKey, string privateKey)
{
    var httpRequest = new HttpRequestMessage(new HttpMethod(httpMethod), request.request);
    httpRequest.Headers.Add("X-BFX-APIKEY", publicKey);

    string json = JsonConvert.SerializeObject(request);
    string json64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
    httpRequest.Headers.Add("X-BFX-PAYLOAD", json64);

    byte[] data = Encoding.UTF8.GetBytes(json64);
    var hashMaker = new HMACSHA384(Encoding.UTF8.GetBytes(privateKey));
    byte[] hash = hashMaker.ComputeHash(data);
    httpRequest.Headers.Add("X-BFX-SIGNATURE", GetHexString(hash));

    return httpRequest;
}
      private async Task<string> SendRequest(GenericRequest request, string httpMethod, string publicKey, string privateKey)
{
    var message = await DefineAndRetrieveResiliencyStrategy().ExecuteAsync(async () =>
    {
        var httpRequest = CreateRequest(request, httpMethod, publicKey, privateKey);
        await client.SendAsync(httpRequest);
    });
    return await message.Content.ReadAsStringAsync();
}

тогда ваша проблема исчезнет.

  • The CreateRequestнесет ответственность за создание новогоHttpRequestMessageвсякий раз, когда он вызывается (либо при первоначальном запросе, либо при любой последующей попытке повтора)
  • The SendRequestотвечает за украшение нисходящей связи предопределенной стратегией и анализ результата любого
  • The HttpClientнастройка должна быть выполнена только один раз и повторно использоваться много раз

с .NET Framework в качестве альтернативы, чтобы сделать его универсальным, вы можете клонировать запрос и сохранять его в contextData, используяIAsyncPolicy.ExecuteAsyncвторой параметр функцииIDictionary<string, object> contextData

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