在构建可上线、可运维的 ASP.NET Core Web API 时,全局异常处理不是“锦上添花”,而是“必备能力”。如果异常没有统一出口,轻则返回格式混乱,重则把堆栈信息、内部路径等敏感数据直接暴露给客户端,既影响体验,也埋下安全隐患。
一个成熟的 API 应该做到:无论哪里抛出异常,最终都能以统一的 JSON 结构返回,并携带准确的 HTTP 状态码,同时完整记录日志。 这正是全局异常处理要解决的问题①。
为什么一定要做全局异常处理?
在实际项目中,异常来源非常多:模型校验失败、数据库连接中断、第三方接口超时、业务规则不满足,甚至是一些不可预期的运行时错误。
如果没有集中处理机制,往往会出现下面这些问题:
控制器里到处都是 try-catch,业务代码和异常处理逻辑混在一起,可读性和维护性都很差。不同接口返回的错误格式不一致,前端不得不写大量分支逻辑去适配。生产环境误把开发模式的详细错误页暴露出去,直接显示堆栈信息。日志记录零散,没有统一入口,排查问题时非常痛苦。
全局异常处理中间件的作用,就是在请求管道的“外层”兜底,拦截所有未处理异常,统一转换成标准响应结构,让系统行为变得可预测、可追踪。
ASP.NET Core 默认的异常处理方式
ASP.NET Core 本身提供了 UseExceptionHandler 中间件。在生产环境中,官方推荐使用它来避免暴露详细错误信息:
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/error");
}
这种方式会把异常重定向到指定的 /error 端点,由该端点统一返回结果。这种做法适合 MVC 页面应用,但在纯 Web API 场景下灵活性不够高。
因此,在 API 项目中,更推荐使用自定义异常处理中间件,完全掌控响应结构和日志行为①。
自定义全局异常中间件(推荐做法)
第一步:创建中间件类
下面是一个标准、简洁的全局异常处理中间件示例。代码已做修正,保证类型声明正确。
public classGlobalExceptionMiddleware
{
privatereadonly RequestDelegate _next;
privatereadonly ILogger<GlobalExceptionMiddleware> _logger;
public GlobalExceptionMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception occurred.");
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
var response = new
{
StatusCode = context.Response.StatusCode,
Message = "An unexpected error occurred."
// 生产环境不要返回 exception.Message 或 StackTrace
};
return context.Response.WriteAsJsonAsync(response);
}
}
这个中间件的逻辑很清晰:先执行后续管道,如果有异常抛出,统一记录日志,然后生成标准 JSON 响应。
第二步:在 Program.cs 中注册
注册顺序很重要,建议放在管道较前位置:
app.UseMiddleware<GlobalExceptionMiddleware>();
app.MapControllers();
这样,控制器或服务层抛出的未捕获异常,都会被统一处理。
根据异常类型返回不同状态码
在企业级 API 中,不同异常应该对应不同的 HTTP 状态码,而不是一律 500。
常见映射关系如下:
参数错误 → 400 Bad Request 未授权 → 401 Unauthorized 资源不存在 → 404 Not Found 未知错误 → 500 Internal Server Error
可以通过模式匹配优雅实现:
context.Response.StatusCode = exception switch
{
KeyNotFoundException => StatusCodes.Status404NotFound,
UnauthorizedAccessException => StatusCodes.Status401Unauthorized,
ArgumentException => StatusCodes.Status400BadRequest,
_ => StatusCodes.Status500InternalServerError
};
在真实项目中,更推荐定义自定义异常,例如 DomainValidationException、BusinessRuleException 等,让异常本身具备清晰语义,而不是到处抛 Exception。
更现代的方式:使用 IExceptionHandler
在较新的 ASP.NET Core 版本中,官方提供了基于 IExceptionHandler 的声明式异常处理方式,更加结构化。
注册方式
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler();
实现处理器
public classGlobalExceptionHandler : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken ct)
{
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
await httpContext.Response.WriteAsJsonAsync(
new ProblemDetails
{
Title = "Server Error",
Detail = "An internal error occurred.",
Status = httpContext.Response.StatusCode
}, ct);
returntrue;
}
}
这种方式天然支持 RFC 7807 标准定义的 ProblemDetails 结构②。客户端可以稳定解析错误格式,也更方便做统一封装。
最佳实践建议
在真实生产环境中,我通常会坚持以下原则。
第一,生产环境绝不返回堆栈信息。异常细节只记录在日志系统中,不出现在 HTTP 响应里。
第二,使用结构化日志。无论是内置 ILogger,还是集成 Serilog,都要记录异常对象本身,而不是简单拼接字符串。
第三,统一响应结构。建议所有错误响应包含状态码、简要描述、错误追踪 ID(如 TraceId),方便和日志系统关联。
第四,设计清晰的领域异常。不要滥用 Exception,应当根据业务场景定义语义明确的异常类型。
第五,结合监控系统。将异常指标上报到监控平台(如 Prometheus、Azure Monitor 等),实现自动告警。
测试验证
可以写一个简单接口来验证异常处理是否生效:
[HttpGet("test-error")]
public IActionResult TestError()
{
throw new InvalidOperationException("Test error");
}
如果配置正确,请求该接口时应返回结构化 JSON,而不是 HTML 错误页,也不会看到堆栈信息。
结语
全局异常处理的本质,是把不可控的异常行为,转化为可控、可观测、可追踪的系统输出。
无论是使用自定义中间件,还是 IExceptionHandler,核心目标都只有一个:让错误变得可管理,而不是让它在系统中随意蔓延。
在企业级 .NET 项目中,这是一项基础工程能力,而不是可选项