ASP.NET Core MVC中客户端中止请求时出现未处理的TaskCancelled异常



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

就.NET而言,这种方法看起来非常清晰、一致和自然。在我看来,不自然和合乎逻辑的是,初始化、填充和"取消"此访问令牌的框架无法处理适当的TaskCancelledException

所以,如果

  1. 我从";ASP.NET核心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:000000002":应用程序引发了未经处理的异常。

System.Threading.Tasks.TaskCanceledException:任务已取消
<lt>gt;

我的期望是,在这种特殊情况下,异常由asp.net处理和吸收,而不是";失败";日志中的记录。

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

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

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

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

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

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

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

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

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

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

我偶然发现了您在文章中描述的相同问题。老实说,中间件可能不是最糟糕的方法。我在Github上的Ocelot API网关中找到了一个很好的例子。

请注意,之后它将返回HTTP499 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}";
}
}

如果你使用多个中间件,它应该是调用列表上的第一个(它是.NET6)

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

相关内容