50

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

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

  • 客户端向代理 1发出 HTTP 请求。
  • 代理 1将 HTTP 请求的内容中继到代理 2
  • 代理 2将 HTTP 请求的内容中继到目标 Web 应用程序
  • 目标 Web 应用程序响应 HTTP 请求并将响应流式传输(分块传输)到代理 2
  • 代理 2将响应返回给代理 1,代理 1 又响应原始调用Client

代理应用程序是使用 .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 事件中捕获错误。不幸的是,异常没有堆栈跟踪。

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

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

4

3 回答 3

68

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

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

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

于 2013-02-25T15:15:30.750 回答
5

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

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

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

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

于 2017-07-26T21:41:52.830 回答
4

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

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

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

于 2013-02-28T05:48:05.087 回答