簡體   English   中英

在 ASP.NET 核心中使用 Polly 重試請求的“流已被消耗”錯誤

[英]'Stream was already consumed' error using Polly to retry requests in ASP.NET Core

I have an ASP.NET Core 3.1 Web API service that is receiving a web request, doing some manipulations to it, and then passing it on to a backend service and returning the response synchronously. 它工作正常,但我想為這些后端請求引入一些重試邏輯,以防出現一些問題。

我正在使用類型化的 HttpClient 並嘗試使用 Polly 來實現重試邏輯: https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory#using-polly-with-ihttpclientfactory

當后端服務工作時,一切似乎都很好,但不幸的是,每當我的后端返回像 500 Internal Server Error 這樣的錯誤時,我都會收到以下異常:

Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware: Error: An unhandled exception has occurred while executing the request.

System.Net.Http.HttpRequestException: An error occurred while sending the request.
 ---> System.InvalidOperationException: The stream was already consumed. It cannot be read again.
   at System.Net.Http.StreamContent.PrepareContent()
   at System.Net.Http.StreamContent.SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken)
   at System.Net.Http.HttpContent.CopyToAsync(Stream stream, TransportContext context, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnection.SendRequestContentAsync(HttpRequestMessage request, HttpContentWriteStream stream, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithNtConnectionAuthAsync(HttpConnection connection, HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.DiagnosticsHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at Polly.Retry.AsyncRetryEngine.ImplementationAsync[TResult](Func`3 action, Context context, CancellationToken cancellationToken, ExceptionPredicates shouldRetryExceptionPredicates, ResultPredicates`1 shouldRetryResultPredicates, Func`5 onRetryAsync, Int32 permittedRetryCount, IEnumerable`1 sleepDurationsEnumerable, Func`4 sleepDurationProvider, Boolean continueOnCapturedContext)
   at Polly.AsyncPolicy`1.ExecuteAsync(Func`3 action, Context context, CancellationToken cancellationToken, Boolean continueOnCapturedContext)
   at Microsoft.Extensions.Http.PolicyHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
   at MyProject.MyController.Routing.HttpMessageRouter.SendRequestAndTrackTiming(Func`1 action, String destinationID) in /mnt/c/Users/myCode/source/repos/MyProject-GitLab/MyController/src/MyController/Routing/HttpMessageRouter.cs:line 59
   at MyProject.MyController.Routing.HttpMessageRouter.SendNewRequest(IMessageWrapper`1 message) in /mnt/c/Users/myCode/source/repos/MyProject-GitLab/MyController/src/MyController/Routing/HttpMessageRouter.cs:line 33
   at MyProject.MyController.Controllers.MyControllerController.Resource(String destinationId) in /mnt/c/Users/myCode/source/repos/MyProject-GitLab/MyController/src/MyController/Controllers/MyControllerController.cs:line 151
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Prometheus.HttpMetrics.HttpRequestDurationMiddleware.Invoke(HttpContext context)
   at Prometheus.HttpMetrics.HttpRequestCountMiddleware.Invoke(HttpContext context)
   at Prometheus.HttpMetrics.HttpInProgressMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

我必須發送請求的代碼如下。 第一種方法實際上只使用了一個委托,因此我可以透明地添加一些圍繞類型化 HttpClient 調用收集的指標。 鍵入的 HttpClient 在此代碼中稱為RoutingClient

public async Task<IActionResult> SendNewRequest(IMessageWrapper<HttpRequestMessage> message)
{
    HttpResponseMessage destinationResponse = await SendRequestAndTrackTiming(() => _client.SendAsync(message.Message), message.DestinationID);
    return CreateSerializeableResponseMessage(destinationResponse);
}

private ResponseMessageResult CreateSerializeableResponseMessage(HttpResponseMessage httpResponse)
{
    ResponseMessageResult responseMessage = new ResponseMessageResult(httpResponse);
    IOutputFormatter[] formattersList = { new HttpResponseMessageOutputFormatter() };
    FormatterCollection<IOutputFormatter> formattersCollection = new FormatterCollection<IOutputFormatter>(formattersList);
    responseMessage.Formatters = formattersCollection;
    return responseMessage;
}

private async Task<HttpResponseMessage> SendRequestAndTrackTiming(Func<Task<HttpResponseMessage>> action, string destinationID)
{
    HttpResponseMessage response = null;

    Stopwatch stopwatch = new Stopwatch();
    try
    {
        stopwatch.Start();
        response = await action();
        return response;
    }
    finally
    {
        stopwatch.Stop();
        HttpStatusCode statusCode = (response != null) ? response.StatusCode : HttpStatusCode.InternalServerError;
        _routedMessageMetricTracker.Histogram.Observe(destinationID, statusCode, stopwatch.Elapsed.TotalSeconds);
    }
}

這是我在 Startup.ConfigureServices() 中的代碼(實際上這是在我定義的擴展方法中):

public static IServiceCollection AddRoutingClient(this IServiceCollection services, RoutingSettings routingSettings)
{
    List<TimeSpan> retryTimeSpans = new List<TimeSpan>();

    // routingSettings.RetrySeconds is just an array of double values.
    foreach (double retrySeconds in routingSettings.RetrySeconds)
    {
        if (retrySeconds >= 0) retryTimeSpans.Add(TimeSpan.FromSeconds(retrySeconds));
    }

    services.AddHttpClient<IRoutingClient, RoutingClient>()
        .AddPolicyHandler((services, request) => HttpPolicyExtensions.HandleTransientHttpError()
            .WaitAndRetryAsync(retryTimeSpans, onRetry: (outcome, timespan, retryAttempt, context) =>
                {
                    services.GetService<ILogger<RoutingClient>>()?
                        .LogWarning($"Delaying for {timespan.TotalMilliseconds}ms, then making retry {retryAttempt}.");
                }
            ));

    return services;
}

我真的很想使用 Polly 方法,因為它看起來很干凈,但我不確定我在這里做錯了什么。 我一定是做錯了什么,因為我希望這對 Polly 來說是一個非常常見的用例,應該得到處理。

事實證明我的問題不是由處理上述響應的代碼引起的。 相反,它實際上是由我操縱請求的代碼引起的。

我正在通過HttpContext.GetHttpRequestMessage()方法將傳入請求作為HttpRequestMessage object 讀取,並且我嘗試重新使用相同的 object 通過調用我鍵入的HttpClient傳遞給后端服務。 但是,該請求的內容 stream 是只讀的,因此我必須按照另一篇帖子的答案中所述復制該HttpRequestMessagehttps://stackoverflow.com/a/34049029/1221718

這是該答案代碼的稍微詳細的版本:

private static async Task<HttpRequestMessage> CloneHttpRequestMessageAsync(HttpRequestMessage request)
{
    HttpRequestMessage copyOfRequest = new HttpRequestMessage(request.Method, request.RequestUri);

    // Copy the request's content (via a MemoryStream) into the cloned object
    var ms = new MemoryStream();
    if (request.Content != null)
    {
        await request.Content.CopyToAsync(ms).ConfigureAwait(false);
        ms.Position = 0;
        copyOfRequest.Content = new StreamContent(ms);

        // Copy the content headers
        if (request.Content.Headers != null)
        {
            foreach (var h in request.Content.Headers)
            {
                copyOfRequest.Content.Headers.Add(h.Key, h.Value);
            }
        }
    }

    copyOfRequest.Version = request.Version;

    foreach (KeyValuePair<string, object> prop in request.Properties)
    {
        copyOfRequest.Properties.Add(prop);
    }

    foreach (KeyValuePair<string, IEnumerable<string>> header in request.Headers)
    {
        copyOfRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);
    }

    return copyOfRequest;
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM