C# Web Api + HttpClient:异步模块或处理程序已完成,而异步操作仍待处理

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/15060214/
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 13:54:34  来源:igfitidea点击:

Web Api + HttpClient: An asynchronous module or handler completed while an asynchronous operation was still pending

c#asp.netasp.net-web-apidotnet-httpclient

提问by Gavin Osborn

I'm writing an application that proxies some HTTP requests using the ASP.NET Web API and I am struggling to identify the source of an intermittent error. It seems like a race condition... but I'm not entirely sure.

我正在编写一个使用 ASP.NET Web API 代理一些 HTTP 请求的应用程序,我正在努力确定间歇性错误的来源。这似乎是一种竞争条件……但我并不完全确定。

Before I go into detail here is the general communication flow of the application:

在我详细介绍之前,这里是应用程序的一般通信流程:

  • Clientmakes a HTTP request to Proxy 1.
  • Proxy 1relays the contents of the HTTP request to Proxy 2
  • Proxy 2relays the contents of the HTTP request to the Target Web Application
  • Target Web Appresponds to the HTTP request and the response is streamed (chunked transfer) to Proxy 2
  • Proxy 2returns the response to Proxy 1which in turn responds to the original calling Client.
  • 客户端代理 1发出 HTTP 请求。
  • 代理 1将 HTTP 请求的内容中继到代理 2
  • 代理 2将 HTTP 请求的内容中继到目标 Web 应用程序
  • 目标 Web 应用程序响应 HTTP 请求并将响应流式传输(分块传输)到代理 2
  • 代理 2将响应返回给代理 1代理 1又响应原始调用Client

The Proxy applications are written in ASP.NET Web API RTM using .NET 4.5. The code to perform the relay looks like so:

代理应用程序是使用 .NET 4.5 在 ASP.NET Web API RTM 中编写的。执行中继的代码如下所示:

//Controller entry point.
public HttpResponseMessage Post()
{
    using (var client = new HttpClient())
    {
        var request = BuildRelayHttpRequest(this.Request);

        //HttpCompletionOption.ResponseHeadersRead - so that I can start streaming the response as soon
        //As it begins to filter in.
        var relayResult = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result;

        var returnMessage = BuildResponse(relayResult);
        return returnMessage;
    }
}

private static HttpRequestMessage BuildRelayHttpRequest(HttpRequestMessage incomingRequest)
{
    var requestUri = BuildRequestUri();
    var relayRequest = new HttpRequestMessage(incomingRequest.Method, requestUri);
    if (incomingRequest.Method != HttpMethod.Get && incomingRequest.Content != null)
    {
       relayRequest.Content = incomingRequest.Content;
    }

    //Copies all safe HTTP headers (mainly content) to the relay request
    CopyHeaders(relayRequest, incomingRequest);
    return relayRequest;
}

private static HttpRequestMessage BuildResponse(HttpResponseMessage responseMessage)
{
    var returnMessage = Request.CreateResponse(responseMessage.StatusCode);
    returnMessage.ReasonPhrase = responseMessage.ReasonPhrase;
    returnMessage.Content = CopyContentStream(responseMessage);

    //Copies all safe HTTP headers (mainly content) to the response
    CopyHeaders(returnMessage, responseMessage);
}

private static PushStreamContent CopyContentStream(HttpResponseMessage sourceContent)
{
    var content = new PushStreamContent(async (stream, context, transport) =>
            await sourceContent.Content.ReadAsStreamAsync()
                            .ContinueWith(t1 => t1.Result.CopyToAsync(stream)
                                .ContinueWith(t2 => stream.Dispose())));
    return content;
}

The error that occurs intermittently is:

间歇性出现的错误是:

An asynchronous module or handler completed while an asynchronous operation was still pending.

异步模块或处理程序已完成,而异步操作仍处于挂起状态。

This error usually occurs on the first few requests to the proxy applications after which the error is not seen again.

此错误通常发生在对代理应用程序的前几个请求中,之后就不会再次出现该错误。

Visual Studio never catches the Exception when thrown. But the error can be caught in the Global.asax Application_Error event. Unfortunately the Exception has no Stack Trace.

Visual Studio 永远不会在抛出异常时捕获异常。但是可以在 Global.asax Application_Error 事件中捕获错误。不幸的是,异常没有堆栈跟踪。

The proxy applications are hosted in Azure Web Roles.

代理应用程序托管在 Azure Web 角色中。

Any help identifying the culprit would be appreciated.

任何确定罪魁祸首的帮助将不胜感激。

采纳答案by Stephen Cleary

Your problem is a subtle one: the asynclambda you're passing to PushStreamContentis being interpreted as an async void(because the PushStreamContentconstructoronly takes Actions as parameters). So there's a race condition between your module/handler completing and the completion of that async voidlambda.

您的问题是一个微妙的问题:async您传递给的lambdaPushStreamContent被解释为async void( 因为PushStreamContent构造函数只将Actions 作为参数)。因此,您的模块/处理程序完成与该async voidlambda的完成之间存在竞争条件。

PostStreamContentdetects the stream closing and treats that as the end of its Task(completing the module/handler), so you just need to be sure there's no async voidmethods that could still run after the stream is closed. async Taskmethods are OK, so this should fix it:

PostStreamContent检测流关闭并将其视为其结束Task(完成模块/处理程序),因此您只需要确保async void在流关闭后没有仍然可以运行的方法。async Task方法没问题,所以这应该解决它:

private static PushStreamContent CopyContentStream(HttpResponseMessage sourceContent)
{
  Func<Stream, Task> copyStreamAsync = async stream =>
  {
    using (stream)
    using (var sourceStream = await sourceContent.Content.ReadAsStreamAsync())
    {
      await sourceStream.CopyToAsync(stream);
    }
  };
  var content = new PushStreamContent(stream => { var _ = copyStreamAsync(stream); });
  return content;
}

If you want your proxies to scale a bit better, I also recommend getting rid of all the Resultcalls:

如果你想让你的代理更好地扩展,我还建议你摆脱所有的Result调用:

//Controller entry point.
public async Task<HttpResponseMessage> PostAsync()
{
  using (var client = new HttpClient())
  {
    var request = BuildRelayHttpRequest(this.Request);

    //HttpCompletionOption.ResponseHeadersRead - so that I can start streaming the response as soon
    //As it begins to filter in.
    var relayResult = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

    var returnMessage = BuildResponse(relayResult);
    return returnMessage;
  }
}

Your former code would block one thread for each request (until the headers are received); by using asyncall the way up to your controller level, you won't block a thread during that time.

您以前的代码会为每个请求阻塞一个线程(直到收到标头);通过一直使用async到您的控制器级别,您将不会在此期间阻塞线程。

回答by Henrik Frystyk Nielsen

A slightly simpler model is that you can actually just use the HttpContents directly and pass them around inside the relay. I just uploaded a sample illustrating how you can rely both requests and responses asynchronously and without buffering the content in a relatively simple manner:

一个稍微简单的模型是您实际上可以直接使用 HttpContents 并将它们传递到中继内部。我刚刚上传了一个示例,说明如何以相对简单的方式异步依赖请求和响应,而不需要缓冲内容:

http://aspnet.codeplex.com/SourceControl/changeset/view/7ce67a547fd0#Samples/WebApi/RelaySample/ReadMe.txt

http://aspnet.codeplex.com/SourceControl/changeset/view/7ce67a547fd0#Samples/WebApi/RelaySample/ReadMe.txt

It is also beneficial to reuse the same HttpClient instance as this allows you to reuse connections where appropriate.

重用相同的 HttpClient 实例也是有益的,因为这允许您在适当的地方重用连接。

回答by Dave Parker

I would like to add some wisdom for anyone else who landed here with the same error, but all of your code seems fine. Look for any lambda expressions passed into functions across the call-tree from where this occurs.

我想为遇到相同错误的其他人添加一些智慧,但您的所有代码似乎都很好。在发生这种情况的整个调用树中查找传递给函数的任何 lambda 表达式。

I was getting this error on a JavaScript JSON call to an MVC 5.x controller action. Everything I was doing up and down the stack was defined async Taskand called using await.

我在对 MVC 5.x 控制器操作的 JavaScript JSON 调用中收到此错误。我在堆栈中上下所做的一切都是async Task使用await.

However, using Visual Studio's "Set next statement" feature I systematically skipped over lines to determine which one caused it. I kept drilling down into local methods until I got to a call into an external NuGet package. The called method took an Actionas a parameter and the lambda expression passed in for this Action was preceded by the asynckeyword. As Stephen Cleary points out above in his answer, this is treated as an async void, which MVC does not like. Luckily said package had *Async versions of the same methods. Switching to using those, along with some downstream calls to the same package fixed the problem.

但是,使用 Visual Studio 的“设置下一条语句”功能,我系统地跳过了几行以确定是哪一行导致了它。我一直深入研究本地方法,直到调用外部 NuGet 包。被调用的方法将 anAction作为参数,并且为此 Action 传入的 lambda 表达式前面是async关键字。正如 Stephen Cleary 在上面的回答中指出的那样,这被视为async voidMVC 不喜欢的 。幸运的是,包有相同方法的 *Async 版本。切换到使用这些,以及对同一个包的一些下游调用解决了这个问题。

I realize this is not a novel solution to the problem, but I passed over this thread a few times in my searches trying to resolve the issue because I thought I didn't have any async voidor async <Action>calls, and I wanted to help someone else avoid that.

我意识到这不是该问题的新颖解决方案,但我在尝试解决该问题的搜索中多次跳过此线程,因为我认为我没有任何电话async voidasync <Action>电话,我想帮助其他人避免这种情况.