GlobalExceptionHander 测试失败将内容复制到流时出错

问题描述 投票:0回答:1

我为最小的 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>();
c# unit-testing asp.net-core exception
1个回答
0
投票

解决方案

您对

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:无法修改响应标头,因为响应已经开始。

但是,由于中间件抑制了次要异常(这样做是正确的!),我们又回到了最初的异常。而那个包裹在一些内部框架结构中的东西最终被抛出:

exception wrapped in edi

© www.soinside.com 2019 - 2024. All rights reserved.