ASP.NET Web API在浏览器取消请求时出现OperationCanceledException异常

129
当用户加载页面时,会发出一个或多个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

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

它似乎不在用户代码之内...

备注:

  • 我正在使用 ASP.NET Web API 2
  • Web API 端点是异步和非异步方法的混合。
  • 无论我在哪里添加错误日志记录,都无法在用户代码中捕获异常

1
我们也在当前版本的Katana库中看到了相同的异常(TaskCanceledException和OperationCanceledException)。 - David McClelland
我找到了更多关于两个异常何时发生的细节,并且发现这个解决方法只适用于其中一个。以下是一些详细信息:https://dev59.com/3mEh5IYBdhLWcg3wrU8N#51514604 - Ilya Chernomordik
10个回答

79

这是 ASP.NET Web API 2 中的一个错误,不幸的是,我认为没有一个总能成功的解决方法。我们已经在我们这边提交了一个bug报告,以便修复它。

问题的本质在于,我们在这种情况下返回了一个取消的任务给ASP.NET,而ASP.NET将取消的任务视为未处理的异常(它会在应用程序事件日志中记录这个问题)。

同时,您可以尝试以下代码。它添加了一个顶级消息处理程序,当取消标记触发时会删除内容。如果响应没有内容,则不会触发该错误。仍有可能在处理程序检查取消标记后但在更高级别的Web API代码执行相同检查之前,客户端会断开连接导致出现问题,但我认为它在大多数情况下都会有所帮助。

David

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

2
作为更新,这确实捕获了一些请求。我们仍然在日志中看到相当多的请求。感谢您的解决方法。期待修复。 - Bates Westmoreland
3
为了暂时解决问题并防止业务恐慌,我能否将以下代码添加到我的ElmahExceptionFilter中? ``public override void OnException(HttpActionExecutedContext context) { if (null != context.Exception && !(context.Exception is TaskCanceledException || context.Exception is OperationCanceledException)) { Elmah.ErrorSignal.FromCurrentContext().Raise(context.Exception); } base.OnException(context); }`` - Ravi
2
我仍然收到错误信息。我已经尝试了上面的建议,还有其他线索吗? - M2012
3
当我尝试上面的建议时,即使请求在传递给SendAsync之前就被取消了(您可以通过在浏览器上按住F5来模拟这种情况),我仍然会收到异常。我通过在调用SendAsync之前添加if (cancellationToken.IsCancellationRequested)检查来解决了这个问题。现在,在浏览器快速取消请求时,异常不再显示。 - seangwright
3
我已经找到了更多关于两种异常发生的细节,并发现这个解决方法只适用于其中一种异常。以下是一些细节: https://dev59.com/3mEh5IYBdhLWcg3wrU8N#51514604 - Ilya Chernomordik
显示剩余14条评论

17

在为WebApi实现异常记录器时,建议扩展System.Web.Http.ExceptionHandling.ExceptionLogger类,而不是创建ExceptionFilter。WebApi内部不会调用已取消请求的ExceptionLoggers的Log方法(但异常过滤器会获取它们)。这是出于设计考虑。

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

这种方法的问题似乎在于,错误仍然会在Global.asax错误处理中弹出...即使它没有被发送到异常处理程序。 - Ilya Chernomordik

15

这里是针对此问题的另一个解决方法。只需在 OWIN 管道开头添加一个自定义 OWIN 中间件来捕获 OperationCanceledException

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

3
我主要从OWIN上下文中遇到了这个错误,这个解决方案更针对它。 - NitinSingh

9
我发现了这个错误的更多细节。有两种可能的异常情况:
  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;
    }
}

我发现唯一的方法是通过检查堆栈跟踪是否包含一些Asp.Net内容来尝试确定错误异常。虽然似乎并不是非常健壮。
附注:这是我如何过滤这些错误的方法:
private static bool IsAspNetBugException(Exception exception)
{
    return
        (exception is TaskCanceledException || exception is OperationCanceledException) 
        &&
        exception.StackTrace.Contains("System.Web.HttpApplication.ExecuteStep");
}

1
在您建议的代码中,您在try块内创建了response变量,并在try块外返回它。这可能行得通吗?另外,您在哪里使用IsAspNetBugException? - Schoof
1
不,那样行不通,这个变量必须在 try/catch 块之外声明,并使用类似于 completed task 的方式进行初始化。这只是一个解决方案的示例,无论如何都不是万无一失的。至于另一个问题,您可以在 Global.Asax 的 OnError 处理程序中使用它。如果您不使用该处理程序记录消息,则无需担心。如果您使用该处理程序,则可以使用此示例将系统中的“非错误”过滤掉。 - Ilya Chernomordik

3
你可以尝试通过web.config更改TPL任务默认的异常处理行为:
<configuration> 
    <runtime> 
        <ThrowUnobservedTaskExceptions enabled="true"/> 
    </runtime> 
</configuration>

在您的Web应用程序中,可以创建一个具有静态构造函数的static类,该类将处理AppDomain.UnhandledException异常。

然而,似乎此异常实际上已经在ASP.NET Web API运行时中的某个地方得到了处理,在您使用代码处理此异常之前就已经处理完毕了。

在这种情况下,您可以通过使用AppDomain.CurrentDomain.FirstChanceException来捕获第一次机会异常,这是如何实现的。我理解这可能不是您想要的。


1
这两种方法都无法让我处理异常。 - Bates Westmoreland
@BatesWestmoreland,甚至没有FirstChanceException?您尝试过使用跨HTTP请求持久化的静态类来处理它吗? - noseratio - open to work
2
我正在尝试解决的问题是捕获并忽略这些异常。使用 AppDomain.UnhandledExceptionAppDomain.CurrentDomain.FirstChanceException 可以让我检查异常,但无法捕获和忽略。我没有看到使用这两种方法标记这些异常为已处理的方式。如果我错了,请纠正我。 - Bates Westmoreland
抛出未被观察的任务异常是一个不好的想法;这就是为什么它默认关闭的原因。每个带有 async void 签名的方法都会抛出一个异常,以及任何没有观察到其结果而被垃圾回收的任务。此外,这甚至与未处理的 TaskCancelledExceptions 无关。如果正在抛出 TaskCancelledException,则已经观察到了任务的结果(取消)。 - Triynko

2
有时在我的Web API 2应用程序中会出现相同的两个异常,但是我可以使用Global.asax.cs中的Application_Error方法和一个通用的异常过滤器捕获它们。
有趣的是,我更喜欢不捕获这些异常,因为我总是记录可能导致应用程序崩溃的所有未处理异常(然而,对我来说这两个异常是无关紧要的,显然不会或者至少不应该导致应用程序崩溃,但我可能错了)。我怀疑这些错误是由于某些超时到期或客户端的显式取消而出现的,但我希望它们能够在ASP.NET框架内部得到处理,而不是作为未处理异常传播到外部。

在我的情况下,这些异常是因为当用户导航到新的URL时,浏览器取消了请求。 - Bates Westmoreland
1
我明白了。在我的情况下,请求是通过WinHTTP API发出的,而不是从浏览器中发出的。 - Gabriel S.

2

这位用户提到了希望在ELMAH中忽略System.OperationCanceledException,并提供了正确方向的链接。自原帖发布以来,ELMAH已经取得了长足的进步,并提供了丰富的功能,可以满足用户的需求。请参考此页面(仍在完善中),该页面概述了编程和声明式(基于配置)方法。

我个人最喜欢的是直接在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>

1
我们一直收到相同的异常,我们尝试使用@dmatson的解决方法,但仍然会出现一些异常。我们一直处理到最近。我们注意到一些Windows日志以惊人的速度增长。
错误文件位于: C:\ Windows \ System32 \ LogFiles \ HTTPERR
大多数错误都是“Timer_ConnectionIdle”。我搜索了一下,似乎即使Web API调用已经完成,连接仍然持续了两分钟以上。
然后我想我们应该尝试在响应中关闭连接,看看会发生什么。
我在SendAsync MessageHandler中添加了response.Headers.ConnectionClose = true;,从我所知道的情况来看,客户端正在关闭连接,我们不再遇到这个问题。
我知道这不是最好的解决方案,但在我们的情况下它起作用了。我也很确定从性能上来说,如果你的API接收到同一个客户端的多个连续调用,你不想这样做。

0

如果您正在使用Sentry SDK,可以在UseSentry()选项中忽略它,如下所示:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder
                    .UseSentry(options =>
                    {
                        options.AddExceptionFilterForType<OperationCanceledException>();
                    })
                    .UseContentRoot(Directory.GetCurrentDirectory())
                    .UseStartup<Startup>();
            });
}

文档链接:https://sentry-docs-git-sentry-ruby-40.sentry.dev/platforms/dotnet/guides/aspnetcore/ignoring-exceptions

根据文档:

SentrySdk.Init(o => o.AddExceptionFilterForType<OperationCanceledException>());

应该也可以与较新版本的ASP.NET WebApi一起使用。


0
这个解决方案是基于@dmatson和其他支持成员提供的解决方案,并且通过这段代码,我成功解决了我的问题。
//add handler to config
    config.MessageHandlers.Add(new CancelledTaskBugWorkaroundMessageHandler());
 
    // create handler
    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.OK);
                }
                return response;

            }
            catch (Exception ex)
            {
                if(IsAspNetBugException(ex))
                    return new HttpResponseMessage(HttpStatusCode.OK);
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }


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

注意:你可以返回自己选择的响应消息,例如内部服务器或OK。个人而言,如果问题不是来自代码,我更喜欢返回OK;然而,如果只是用户取消了一个操作,并且我们除了记录错误之外无法做任何其他事情,那么也可以返回OK。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接