ASP.NET Web API OperationCanceledException when browser cancels the request

当用户加载页面时,它会发出一个或多个 ajax 请求,这些请求会触发 ASP.NET Web API 2控制器。如果用户导航到另一个页面,在这些 ajax 请求完成之前,浏览器将取消这些请求。然后,我们的 ELMAH HttpModule 为每个取消的请求记录两个错误:

错误1:

System.Threading.Tasks.TaskCanceledException: A task was canceled.
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at System.Web.Http.Controllers.ApiControllerActionInvoker.<InvokeActionAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.Filters.AuthorizationFilterAttribute.<ExecuteAuthorizationFilterAsyncCore>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at System.Web.Http.Controllers.ExceptionFilterResult.<ExecuteAsync>d__0.MoveNext()

错误2:

System.OperationCanceledException: The operation was canceled.
at System.Threading.CancellationToken.ThrowIfCancellationRequested()
at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__1b.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.WebHost.HttpControllerHandler.<CopyResponseAsync>d__7.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.WebHost.HttpControllerHandler.<ProcessRequestAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.TaskAsyncHelper.EndTask(IAsyncResult ar)
at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

查看堆栈跟踪,我发现异常是从这里抛出的: https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Http.WebHost/HttpControllerHandler.cs#L413

我的问题是: 我如何处理和忽略这些异常?

似乎不在用户代码范围内。

备注:

  • I am using ASP.NET Web API 2
  • Web API 端点是异步和非异步方法的混合体。
  • 无论在哪里添加错误日志记录,都无法在用户代码中捕获异常
82338 次浏览

你可以尝试改变 默认的 TPL 任务异常处理行为web.config:

<configuration>
<runtime>
<ThrowUnobservedTaskExceptions enabled="true"/>
</runtime>
</configuration>

然后在 Web 应用程序中有一个 static类(带有 static构造函数) ,它将处理 AppDomain.UnhandledException

然而,看起来 this exception is actually getting handled somewhere inside ASP.NET Web API runtime,在你有机会用你的代码处理它之前。

在这种情况下,您应该能够捕获它作为第一次机会异常,与 AppDomain.CurrentDomain.FirstChanceException这就是方法。我知道这可能不是你想要的。

在我的 Web API 2应用程序中,有时会遇到相同的两个异常,但是我可以使用 Global.asax.cs中的 Application_Error方法和通用 异常过滤器捕获它们。

有趣的是,尽管我不喜欢捕捉这些异常,因为我总是记录所有可能导致应用程序崩溃的未处理异常(然而,这两个对我来说无关紧要,显然不会或至少不应该崩溃它,但我可能错了)。我怀疑这些错误是由于某些超时过期或显式取消客户端而出现的,但我希望它们在 ASP.NET 框架内被处理,而不是作为未处理的异常在框架外传播。

This is a bug in ASP.NET Web API 2 and unfortunately, I don't think there's a workaround that will always succeed. We filed a 臭虫 to fix it on our side.

最终,问题在于,在这种情况下,我们将取消的任务返回给 ASP.NET,而 ASP.NET 将取消的任务视为未处理的异常(它将问题记录在 Application 事件日志中)。

与此同时,您可以尝试下面的代码。它添加了一个顶级消息处理程序,该处理程序在取消令牌触发时删除内容。如果响应没有内容,就不应该触发 bug。这种情况发生的可能性仍然很小,因为客户机可能会在消息处理程序检查取消令牌之后、但在更高级别的 Web API 代码进行同样检查之前断开连接。但我觉得在大多数情况下会有帮助。

大卫

config.MessageHandlers.Add(new CancelledTaskBugWorkaroundMessageHandler());


class CancelledTaskBugWorkaroundMessageHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);


// Try to suppress response content when the cancellation token has fired; ASP.NET will log to the Application event log if there's content in this case.
if (cancellationToken.IsCancellationRequested)
{
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}


return response;
}
}

在为 WebApi 实现异常日志记录器时,建议扩展 System.Web.Http.ExceptionHandling.ExceptionLogger类,而不是创建 ExceptionFilter。WebApi 内部不会为取消的请求调用 ExceptionLoggers 的 Log 方法(但是,异常筛选器会得到它们)。这是设计好的。

HttpConfiguration.Services.Add(typeof(IExceptionLogger), myWebApiExceptionLogger);

我们已经收到了相同的异常,我们尝试使用@dmatson 的工作区,但仍然会有一些异常通过。我们一直在处理这件事。我们注意到一些 Windows 日志以惊人的速度增长。

Error files located in: C: Windows System32 LogFiles HTTPERR

大多数错误都是针对“ Timer _ ConnectionIdle”的。我搜索了一下,似乎即使 web api 调用已经结束,连接仍然比原来的连接持续了两分钟。

然后我想我们应该尝试在响应中关闭连接,看看会发生什么。

I added response.Headers.ConnectionClose = true; to the SendAsync MessageHandler and from what I can tell the clients are closing the connections and we aren't experiencing the issue any more.

我知道这不是最好的解决办法,但对我们来说很有效。我也很确定,如果你的 API 接收到来自同一个客户端的多个调用,从性能方面来说,这不是你想要做的事情。

这里还有一个解决这个问题的方法。只需在捕获 OperationCanceledException的 OWIN 管道的开头添加一个自定义 OWIN 中间件:

#if !DEBUG
app.Use(async (ctx, next) =>
{
try
{
await next();
}
catch (OperationCanceledException)
{
}
});
#endif

关于这个错误,我已经找到了更多的细节。有两个可能发生的例外:

  1. OperationCanceledException
  2. TaskCanceledException

第一种情况发生在控制器中的代码执行时连接被丢弃的情况下(或者也可能是一些系统代码)。而第二种情况发生在连接被删除时,执行在属性内部(例如 AuthorizeAttribute)。

因此,提供的 workaround有助于部分缓解第一个异常,但对第二个异常没有任何帮助。在后一种情况下,TaskCanceledException发生在 base.SendAsync调用本身期间,而不是将取消令牌设置为 true。

我认为有两种方法可以解决这些问题:

  1. 只要忽略 global.asax 中的两个异常,那么问题就来了,是否有可能突然忽略一些重要的东西?
  2. 在处理程序中执行额外的 try/catch 操作(尽管它不是防弹的 + 仍然有可能我们忽略的 TaskCanceledException是我们想要记录的。

config.MessageHandlers.Add(new CancelledTaskBugWorkaroundMessageHandler());


class CancelledTaskBugWorkaroundMessageHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
try
{
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);


// Try to suppress response content when the cancellation token has fired; ASP.NET will log to the Application event log if there's content in this case.
if (cancellationToken.IsCancellationRequested)
{
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
}
catch (TaskCancellationException)
{
// Ignore
}


return response;
}
}

我发现我们可以精确定位错误异常的唯一方法是检查 stacktrace 是否包含一些 Asp。网上的东西。不过看起来不是很健壮。

另外,我是这样过滤这些错误的:

private static bool IsAspNetBugException(Exception exception)
{
return
(exception is TaskCanceledException || exception is OperationCanceledException)
&&
exception.StackTrace.Contains("System.Web.HttpApplication.ExecuteStep");
}

The OP mentioned the desire to ignore System.OperationCanceledException within ELMAH and even provides a link in the right direction. ELMAH has come a long way since the original post and it provides a rich capability to do exactly what the OP is requesting. Please see 这一页 (still being completed) which outlines the programmatic and declarative (configuration-based) approaches.

我个人最喜欢的是直接在 Web.config 中使用的声明性方法。按照上面链接的指南来学习如何为基于配置的 ELMAH 异常过滤设置 Web.config。为了专门过滤掉 System.OperationCanceledException,可以使用 is-type断言:

<configuration>
...
<elmah>
...
<errorFilter>
<test>
<or>
...
<is-type binding="BaseException" type="System.OperationCanceledException" />
...
</or>
</test>
</errorFilter>
</elmah>
...
</configuration>