【问题标题】:Unhandled TaskCancelledException when request is aborted by client in ASP.NET Core MVC在 ASP.NET Core MVC 中客户端中止请求时出现未处理的 TaskCancelledException
【发布时间】:2021-10-17 18:32:11
【问题描述】:

ASP.NET Core MVC 提供了在请求被客户端中止时处理情况的方法。框架传递了CancellationToken,可以通过HttpContext.RequestAborted属性访问,也可以绑定到控制器的action中。

在 .NET 方面,这种方法看起来非常清晰、一致和自然。对我来说看起来不自然和不合逻辑的是那个框架,它初始化、填充和“取消”这个访问令牌没有处理适当的TaskCancelledException

如果

  1. 我从“ASP.NET Core Web API”模板创建一个新项目,

  2. 添加带有CancellationToken 参数的操作,如下所示:

     [HttpGet("Delay")]
     public async Task<IActionResult> GetDelayAsync(CancellationToken cancellationToken)
     {
         await Task.Delay(30_000, cancellationToken);
         return Ok();
     }
    
  3. 然后通过邮递员发送请求并在完成前取消它

然后应用程序在日志中记录这个错误:

失败:Microsoft.AspNetCore.Server.Kestrel[13]

连接 ID“0HMCHB3SQHQQR”,请求 ID“0HMCHB3SQHQQR:00000002”:应用程序抛出了未处理的异常。

System.Threading.Tasks.TaskCanceledException:任务被取消。
>

我的期望是这种特殊情况下的异常由 asp.net 处理和吸收,日志中没有“失败”记录。

错误行为应该与同步操作相同:

    [HttpGet("Delay")]
    public IActionResult GetDelay()
    {
        Thread.Sleep(30_000);
        return Ok();
    }

当请求中止时,此实现不会在日志中记录任何错误。

从技术上讲,异常过滤器可以吸收和隐藏异常,但这种方法看起来很奇怪且过于复杂。至少因为这是常规情况,为任何应用程序编写代码没有任何意义。另外,我想隐藏“当客户端对响应不感兴趣时​​由中止请求引起的异常”,并且与其他未处理的 TaskCancelledException 相关的行为应该保持原样......

我想知道当请求被客户端中止时,它应该如何以及何时正确处理和吸收异常?

有很多文章如何访问取消令牌,但是我找不到任何明确的陈述来回答我的问题。

【问题讨论】:

    标签: asp.net-core asp.net-core-mvc


    【解决方案1】:

    来自https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-cancellation

    如果您正在等待转换到已取消状态的任务,则 System.Threading.Tasks.TaskCanceledException 异常(包装在 AggregateException 异常)被抛出。请注意,此异常 表示取消成功而不是故障情况。 因此,任务的 Exception 属性返回 null。

    这就是这个块不抛出的原因(没有与取消令牌相关联的等待任务):

    [HttpGet("Delay")]
            public IActionResult GetDelay(CancellationToken cancellationToken)
            {
                Thread.Sleep(30_000);
                return Ok();
            }
    

    【讨论】:

    • 当然,没有任务 - 没有与任务相关的异常。我试图用“错误方式”说的是,我希望同步和异步操作具有相同的行为(日志中的错误数量相同)。稍微更新的问题...
    【解决方案2】:

    我偶然发现了您在帖子中描述的相同问题。说真的,中间件可能不是最糟糕的方法。我在 Github 上找到了很好的 example in Ocelot API gateway

    注意它会在之后返回 HTTP 499 Client Closed Request。 您可以修改它以不写入日志。

    /// <summary>
    /// Catches all unhandled exceptions thrown by middleware, logs and returns a 500.
    /// </summary>
    public class ExceptionHandlerMiddleware : OcelotMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IRequestScopedDataRepository _repo;
    
        public ExceptionHandlerMiddleware(RequestDelegate next,
            IOcelotLoggerFactory loggerFactory,
            IRequestScopedDataRepository repo)
                : base(loggerFactory.CreateLogger<ExceptionHandlerMiddleware>())
        {
            _next = next;
            _repo = repo;
        }
    
        public async Task Invoke(HttpContext httpContext)
        {
            try
            {
                httpContext.RequestAborted.ThrowIfCancellationRequested();
                var internalConfiguration = httpContext.Items.IInternalConfiguration();
                TrySetGlobalRequestId(httpContext, internalConfiguration);
                Logger.LogDebug("ocelot pipeline started");
                await _next.Invoke(httpContext);
            }
            catch (OperationCanceledException) when (httpContext.RequestAborted.IsCancellationRequested)
            {
                Logger.LogDebug("operation canceled");
                if (!httpContext.Response.HasStarted)
                {
                    httpContext.Response.StatusCode = 499;
                }
            }
            catch (Exception e)
            {
                Logger.LogDebug("error calling middleware");
                var message = CreateMessage(httpContext, e);
                Logger.LogError(message, e);
                SetInternalServerErrorOnResponse(httpContext);
            }
            Logger.LogDebug("ocelot pipeline finished");
        }
    
        private void TrySetGlobalRequestId(HttpContext httpContext, IInternalConfiguration configuration)
        {
            var key = configuration.RequestId;
            if (!string.IsNullOrEmpty(key) && httpContext.Request.Headers.TryGetValue(key, out var upstreamRequestIds))
            {
                httpContext.TraceIdentifier = upstreamRequestIds.First();
            }
            _repo.Add("RequestId", httpContext.TraceIdentifier);
        }
    
        private void SetInternalServerErrorOnResponse(HttpContext httpContext)
        {
            if (!httpContext.Response.HasStarted)
            {
                httpContext.Response.StatusCode = 500;
            }
        }
    
        private string CreateMessage(HttpContext httpContext, Exception e)
        {
            var message =
                $"Exception caught in global error handler, exception message: {e.Message}, exception stack: {e.StackTrace}";
            if (e.InnerException != null)
            {
                message =
                    $"{message}, inner exception message {e.InnerException.Message}, inner exception stack {e.InnerException.StackTrace}";
            }
            return $"{message} RequestId: {httpContext.TraceIdentifier}";
        }
    }
    

    如果您使用多个中间件,它应该在调用列表中排在第一位(它是 .NET 6)

    app.UseMiddleware(typeof(ExceptionHandlerMiddleware));
    
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    

    【讨论】:

      猜你喜欢
      • 2021-12-13
      • 1970-01-01
      • 1970-01-01
      • 2017-07-10
      • 1970-01-01
      • 1970-01-01
      • 2016-08-28
      • 2010-11-10
      • 1970-01-01
      相关资源
      最近更新 更多