.NET 6中Should.ThrowAsync无法捕获HttpClient.GetAsync请求异常
问题分析与解决方案
问题背景
我们有一个接口测试,目标是验证当接口抛出异常时返回500 InternalServerError。但从.NET Core 3.1升级到.NET 6后,Azure DevOps YAML部署管道运行测试时,会报告授权策略代码路径中的未处理异常,导致测试失败;而在Visual Studio 2022 Test Explorer本地运行测试时,虽然异常确实抛出,但测试仍能通过(因为返回了500状态码)。
相关代码如下:
接口端点
[HttpGet] [Route("machines/{idMachine:guid}/admin")] [Authorize(Policy = Policies.RequireMachineCustomerCheck)] public IActionResult MachineEditor(Guid idMachine) { // 机器查询逻辑... if (machine == null) { throw new Exception("Exception"); } }
测试代码
[Fact] public async Task MachineNotFoundShouldThrowException() { var idMachine = Guid.NewGuid(); var uri = new Uri($"/machines/{idMachine}/admin", UriKind.Relative); var httpClient = new HttpClient(); var response = await httpClient.GetAsync(uri); response.StatusCode.ShouldBe(HttpStatusCode.InternalServerError); }
授权Handler代码
public class MachineCustomerCheckHandler : AuthorizationHandler<MachineCustomerCheck> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MachineCustomerCheck requirement) { // 机器查询逻辑... if (machine == null) { throw new Exception("Exception"); } } }
核心原因
- .NET 6中间件异常处理机制变化:.NET 6对未处理异常的捕获逻辑做了调整,授权中间件中抛出的异常在Azure DevOps的测试运行上下文里,会被测试运行器识别为未处理异常上报,而不是被框架自动转换为500响应。
- 授权Handler的错误实现:ASP.NET Core授权Handler的设计初衷是通过
context.Succeed()/context.Fail()标记授权结果,而非直接抛出异常。直接抛出异常会打破中间件管道的正常流程。 - 测试方式不规范:直接使用
HttpClient调用接口,没有模拟完整的ASP.NET Core管道,导致异常处理中间件无法正常捕获并转换异常。
解决方案
1. 修复授权Handler的逻辑
将授权Handler中的抛出异常改为标记授权失败,或把机器不存在的检查逻辑移到接口方法内:
public class MachineCustomerCheckHandler : AuthorizationHandler<MachineCustomerCheck> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MachineCustomerCheck requirement) { // 从路由获取机器ID if (!context.Resource is HttpContext httpContext || !httpContext.Request.RouteValues.TryGetValue("idMachine", out var idValue) || !Guid.TryParse(idValue.ToString(), out Guid idMachine)) { context.Fail(); return Task.CompletedTask; } // 机器查询逻辑 var machine = // 你的查询代码 if (machine == null) { // 标记授权失败,框架会返回403 Forbidden // 如果需要返回500,可将机器检查逻辑移到接口方法中 context.Fail(); return Task.CompletedTask; } context.Succeed(requirement); return Task.CompletedTask; } }
2. 使用WebApplicationFactory搭建测试服务器
通过WebApplicationFactory模拟完整的ASP.NET Core管道,确保异常被异常处理中间件正确捕获:
public class MachineTests : IClassFixture<WebApplicationFactory<Program>> { private readonly WebApplicationFactory<Program> _factory; public MachineTests(WebApplicationFactory<Program> factory) { _factory = factory; } [Fact] public async Task MachineNotFoundShouldReturn500() { var idMachine = Guid.NewGuid(); var client = _factory.CreateClient(); var response = await client.GetAsync($"/machines/{idMachine}/admin"); response.StatusCode.ShouldBe(HttpStatusCode.InternalServerError); } }
3. 确保异常处理中间件顺序正确
在Program.cs中,将异常处理中间件放在管道最前端,确保能捕获所有后续中间件的异常:
var builder = WebApplication.CreateBuilder(args); // 注册服务... builder.Services.AddAuthorization(options => { options.AddPolicy(Policies.RequireMachineCustomerCheck, policy => policy.Requirements.Add(new MachineCustomerCheck())); }); var app = builder.Build(); // 异常处理中间件必须放在最前面 if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); app.UseHsts(); } // 其他中间件... app.UseAuthorization(); app.MapControllers(); app.Run();
本地与Azure DevOps表现差异说明
本地VS测试环境中,开发模式的异常处理中间件会自动捕获管道内的异常并转换为响应,且测试运行器不会将此类异常标记为未处理;而Azure DevOps的测试运行环境通常使用生产模式配置,对未处理异常的检测更严格,导致异常被上报为测试失败。
内容的提问来源于stack exchange,提问作者Jack Cotterill
相关产品推荐
相关产品推荐

