Web Api + HttpClient:在异步操作仍处于挂起状态时完成的异步模块或处理程序

时间:2013-02-25 04:38:38

标签: c# asp.net asp.net-web-api dotnet-httpclient

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

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

  • 客户端代理1 发出HTTP请求。
  • 代理1 将HTTP请求的内容中继到代理2
  • 代理服务器2 将HTTP请求的内容中继到目标Web应用程序
  • 目标网络应用程序响应HTTP请求,并将响应流式传输(分块传输)到代理服务器2
  • 代理服务器2 会将响应返回代理服务器1 ,然后响应原始呼叫客户端

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

间歇性发生的错误是:

  

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

此错误通常发生在对代理应用程序的前几个请求之后,之后不会再看到错误。

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

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

任何帮助确定罪魁祸首都将受到赞赏。

3 个答案:

答案 0 :(得分:65)

您的问题非常微妙:您传递给async的{​​{1}} lambda被解释为PushStreamContent(因为PushStreamContent constructor仅需async void s作为参数)。因此,您的模块/处理程序完成与Action lambda的完成之间存在竞争条件。

async void检测到流关闭并将其视为其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; } 次呼叫:

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; } } 一直到控制器级别,在此期间不会阻塞线程。

答案 1 :(得分:4)

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

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

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

答案 2 :(得分:3)

我想为其他任何登陆此错误的人添加一些智慧,但所有代码似乎都没问题。查找在调用树中传递给函数的任何lambda表达式。

我在对MVC 5.x控制器操作的JavaScript JSON调用上收到此错误。我在堆栈中上下做的所有内容都定义为async Task,并使用await进行调用。

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

我意识到这不是解决这个问题的新方法,但是我在搜索中尝试解决了这个问题几次,因为我认为我没有任何async void或{{1电话,我想帮助其他人避免这种情况。