×

ASP.NET Core API 的“最后一道防线”:全局异常处理

独孤求败 独孤求败 发表于2026-02-27 10:41:41 浏览37 评论0

抢沙发发表评论

在构建可上线、可运维的 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
};

在真实项目中,更推荐定义自定义异常,例如 DomainValidationExceptionBusinessRuleException 等,让异常本身具备清晰语义,而不是到处抛 Exception


更现代的方式:使用 IExceptionHandler

在较新的 ASP.NET Core 版本中,官方提供了基于 IExceptionHandler 的声明式异常处理方式,更加结构化。

注册方式

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

app.UseExceptionHandler();

实现处理器

public classGlobalExceptionHandler : IExceptionHandler
{
    public async ValueTask<boolTryHandleAsync(
        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 项目中,这是一项基础工程能力,而不是可选项


群贤毕至

访客