我为最小的 API 创建了
GlobalExceptionHandler
和一些单元测试来验证处理程序。
我的处理程序看起来像这样:
internal class GlobalExceptionHandler(ILoggerWrapper<GlobalExceptionHandler> logger) : IExceptionHandler
{
/// <inheritdoc cref="IExceptionHandler.TryHandleAsync"/>
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
ProblemDetails problemDetails = new()
{
Status = StatusCodes.Status500InternalServerError,
//TODO: Build a visible endpoint to describe the exception CV-27246
Type = "https://httpstatuses.com/500",
Detail = exception.StackTrace
};
Action<Exception> logAction = logger.LogCritical;
switch (exception)
{
case OperationCanceledException:
problemDetails.Title = "The operation has been canceled.";
break;
case OutOfMemoryException:
problemDetails.Title = "There was an error, Out of memory. Please see logs for more information.";
break;
case StackOverflowException:
problemDetails.Title = "There was an error, StackOverflow. Please see logs for more information.";
break;
case SEHException:
problemDetails.Title = "There was an error, SEHException. Please see logs for more information.";
break;
default:
logAction = logger.LogError;
problemDetails.Status = StatusCodes.Status417ExpectationFailed;
//TODO: Build a visible endpoint to describe the exception CV-27246
problemDetails.Type = "https://httpstatuses.com/417";
problemDetails.Title = $"{exception.Message} Please see logs for more information.";
break;
}
logAction(exception);
httpContext.Response.StatusCode = problemDetails.Status.HasValue ?
problemDetails.Status.Value :
StatusCodes.Status500InternalServerError;
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
var canContinue = logAction != logger.LogCritical;
return canContinue;
}
}
当抛出上述错误之一时,它正在工作并返回 false。
在我的集成测试中我添加了:
protected override void ConfigureWebHost(IWebHostBuilder builder) {
builder.Configure(app =>
{
app.UseExportService();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("throw-out-of-memory", UnhandledExceptionEndpoints.ThrowOutOfMemory);
endpoints.MapGet("throw-stack-overflow", UnhandledExceptionEndpoints.ThrowStackOverflow);
endpoints.MapGet("throw-seh", UnhandledExceptionEndpoints.ThrowSEHException);
endpoints.MapGet("throw-handeled", UnhandledExceptionEndpoints.ThrowHandledException);
});
});
}
这只是抛出所要求的异常。
单元测试如下所示:
[Fact]
public async Task OperationCanceledException_ShouldReturn500()
{
//Arrange
var client = _factory.CreateClient();
HttpResponseMessage? response = null;
//Act
response = await client.GetAsync("throw-out-of-memory");
var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>();
//Assert
response.IsSuccessStatusCode.Should().BeFalse();
problemDetails.Status.Should().Be(500);
problemDetails.Type.Should().Be("https://httpstatuses.com/500");//TODO: Build a visible endpoint to describe the exception CV-27246
problemDetails.Title.Should().Be("There was an error, Out of memory. please see logs for more information.");
}
直到最近,所有这些都运行正常。它似乎在 CI/CD 上运行得很好,但是当我在本地运行它时,出现以下错误:
System.Net.Http.HttpRequestException Error while copying content to a stream.
at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)
at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
at ExportService.Tests.Intergration.Middleware.GlobalExceptionHandlerTests.OperationCanceledException_ShouldReturn500() in C:\Users\james.tays\source\repos\Components\src\ExportService\ExportService.Tests.Intergration\Middleware\GlobalExceptionHandlerTests.cs:line 72
at Xunit.Sdk.TestInvoker`1.<>c__DisplayClass47_0.<<InvokeTestMethodAsync>b__1>d.MoveNext() in /_/src/xunit.execution/Sdk/Frameworks/Runners/TestInvoker.cs:line 259
--- End of stack trace from previous location ---
at Xunit.Sdk.ExecutionTimer.AggregateAsync(Func`1 asyncAction) in /_/src/xunit.execution/Sdk/Frameworks/ExecutionTimer.cs:line 48
at Xunit.Sdk.ExceptionAggregator.RunAsync(Func`1 code) in /_/src/xunit.core/Sdk/ExceptionAggregator.cs:line 90
System.IO.IOException
at Microsoft.AspNetCore.TestHost.ResponseBodyReaderStream.CheckAborted()
at Microsoft.AspNetCore.TestHost.ResponseBodyReaderStream.ReadAsync(Memory`1 buffer, CancellationToken cancellationToken)
at System.IO.Stream.<CopyToAsync>g__Core|27_0(Stream source, Stream destination, Int32 bufferSize, CancellationToken cancellationToken)
at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)
System.OutOfMemoryException OutOfMemory
at ExportService.Tests.Intergration.Endpoints.UnhandledExceptionEndpoints.ThrowOutOfMemory() in C:\Users\james.tays\source\repos\Components\src\ExportService\ExportService.Tests.Intergration\Endpoints\UnhandledExceptionEndpoints.cs:line 15
at lambda_method19(Closure, Object, HttpContext)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddlewareImpl.Invoke(HttpContext context)
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddlewareImpl.HandleException(HttpContext context, ExceptionDispatchInfo edi)
at Microsoft.AspNetCore.TestHost.HttpContextBuilder.<>c__DisplayClass23_0.<<SendAsync>g__RunRequestAsync|0>d.MoveNext()
在调试时我永远无法做到:
var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>();
您能解释一下为什么这个异常没有被处理吗?我刚刚在处理程序中返回 true,但它仍然具有相同的行为。 在我的服务中,我确实有以下几点:
services.AddProblemDetails();
services.AddExceptionHandler<GlobalExceptionHandler>();
您对
IExceptionHandler
的实施不正确。 TryHandleAsync()
方法的结果应该是一个布尔标志,指示是否应调用下一个异常处理程序并尝试处理异常,而不是请求管道是否可以根据错误严重性继续。
所以,如果你回来:
false
- 这意味着您的异常处理程序没有处理异常。如果您只想记录一些有关异常的内容并且对处理它不感兴趣,这可能很有用。true
- 这意味着您的异常处理程序成功处理了异常,并且不需要进一步的异常。这就是为什么在此之后不会执行其他异常处理程序。另外:
如果任何异常处理程序均未处理异常,则控制权将回退到中间件的默认行为和选项。 ~ ASP.NET Core 文档
如果您写信给回复,则应始终返回
true
:
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
return true;
默认行为就是您的情况所发生的情况,尽管我必须承认我花了一段时间才揭开它的面纱。当你查看这段
ExceptionHandlerMiddleware
实现的源代码时(完整源代码位于GitHub):
string? handler = null;
var handled = false;
foreach (var exceptionHandler in _exceptionHandlers)
{
handled = await exceptionHandler.TryHandleAsync(context, edi.SourceException, context.RequestAborted);
if (handled)
{
handler = exceptionHandler.GetType().FullName;
break;
}
}
您可以看到这里的逻辑循环访问异常处理程序。但由于只注册了一个处理程序,而且是您自定义的处理程序,因此我们退出 for every 并将
handled
变量设置为 false
。
这允许满足下面的 if 语句并陷入其中:
if (!handled)
{
if (_options.ExceptionHandler is not null)
{
await _options.ExceptionHandler!(context);
}
else
{
handled = await _problemDetailsService!.TryWriteAsync(new()
{
HttpContext = context,
AdditionalMetadata = exceptionHandlerFeature.Endpoint?.Metadata,
ProblemDetails = { Status = DefaultStatusCode },
Exception = edi.SourceException,
});
if (handled)
{
handler = _problemDetailsService.GetType().FullName;
}
}
}
选项中的异常处理程序为空,因此控制回退到默认行为并尝试将标准问题详细信息写入响应。但它未能这样做,因为此时响应已经写好了。您的自定义异常处理程序
GlobalExceptionHandler
已经设置了响应状态代码、响应标头并写入响应流。结果,引发了另一个异常:
System.InvalidOperationException:无法修改响应标头,因为响应已经开始。
但是,由于中间件抑制了次要异常(这样做是正确的!),我们又回到了最初的异常。而那个包裹在一些内部框架结构中的东西最终被抛出: