ASP.NET Core 全局异常处理:优雅地响应 404 及更多错误

在现代 Web 应用开发中,程序的健壮性和用户体验至关重要。一个未经处理的异常可能导致应用崩溃,而生硬的错误页面则会让用户感到沮丧。ASP.NET Core 提供了强大而灵活的全局异常处理机制,帮助开发者优雅地捕获、记录和响应各种运行时错误,包括常见的 404 Not Found 状态码。本文将深入探讨 ASP.NET Core 中全局异常处理的多种方法,助你构建更稳定、更用户友好的 Web 应用。

1. UseExceptionHandler:捕获未处理异常的利器

UseExceptionHandler 是 ASP.NET Core 提供的一个非常方便的中间件,用于捕获应用中未处理的异常。当发生未处理异常时,它会将请求重定向到一个指定的错误处理路径,从而避免应用直接崩溃,并允许你向用户展示一个友好的错误页面。

在 Program.cs 里这样配置 UseExceptionHandler:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage(); // 开发环境显示详细错误信息
}
else
{
    app.UseExceptionHandler("/Home/Error"); // 生产环境重定向到统一错误处理页面
    app.UseHsts(); // 生产环境建议启用 HSTS
}

// 其他中间件...

在上述代码中,当发生未处理异常时,请求将被重定向到 /Home/Error 路径。你需要在 HomeController 中创建一个 Error Action 来处理这个请求,并可以访问 IExceptionHandlerFeature 来获取异常的详细信息:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerFeature>();
        if (exceptionFeature != null)
        {
            _logger.LogError(exceptionFeature.Error, "An unhandled exception occurred.");
            // 可以根据异常类型或用户角色显示不同的错误信息
            ViewBag.ErrorMessage = "抱歉,服务器开小差了,请稍后再试。";
        }
        return View();
    }
}

通过这种方式,即使出现意外错误,用户也能看到一个友好的提示页面,而不是难看的错误堆栈。

2. 优雅处理 404 Not Found:UseStatusCodePages

UseExceptionHandler 主要用于处理未捕获的服务器端异常,但对于客户端发起的请求路径不存在(即 404 Not Found)的情况,它并不会介入。这时,UseStatusCodePages 就派上了用场。它允许你为指定的 HTTP 状态码(如 404、400 等)配置自定义的响应内容。

UseStatusCodePages 提供了多种配置方式:

简单文本响应:

app.UseStatusCodePages(); // 默认显示简单的状态码和描述
// 或
app.UseStatusCodePages("出错了!状态码:{0}"); // 自定义文本模板

重定向到指定路径:

app.UseStatusCodePagesWithRedirects("/Error/StatusCode?code={0}");

这种方式会将 404 请求重定向到 /Error/StatusCode 路径,并在查询字符串中传递状态码。你需要在 ErrorController 中创建相应的 Action 来处理。

直接执行委托:

app.UseStatusCodePages(async context =>
{
    context.HttpContext.Response.ContentType = "text/plain";
    await context.HttpContext.Response.WriteAsync($"状态码:{context.HttpContext.Response.StatusCode}。页面找不到了。");
});

这种方式直接在中间件中生成响应内容,更加灵活。

推荐:重定向到统一错误页面并传递状态码

通常,我们会将 404 和其他错误状态码的处理统一到 Error 页面中,通过查询参数传递状态码,以便在页面中显示不同的提示信息。

app.UseStatusCodePagesWithReExecute("/Home/Error", "?statusCode={0}");

在 HomeController 的 Error Action 中,可以这样获取状态码:

public IActionResult Error(int? statusCode = null)
{
    if (statusCode.HasValue)
    {
        if (statusCode.Value == 404)
        {
            ViewBag.ErrorMessage = "抱歉,你访问的页面不存在。";
        }
        else
        {
            ViewBag.ErrorMessage = $"抱歉,发生了错误。状态码:{statusCode.Value}";
        }
    }
    else
    {
        ViewBag.ErrorMessage = "抱歉,服务器开小差了,请稍后再试。";
    }
    return View();
}

UseStatusCodePagesWithReExecute 和 UseStatusCodePagesWithRedirects 的区别在于,WithReExecute 是在内部重新执行管道,URL 不会改变,而 WithRedirects 会发送一个 302 重定向,URL 会发生变化。对于 404 页面,通常更推荐使用 WithReExecute 来保持原始 URL,这也有利于 SEO。

3. 自定义中间件:更细粒度的控制

对于更复杂的异常处理场景,例如需要在异常发生时执行特定的业务逻辑(如发送邮件通知、记录到特定日志系统),或者根据异常类型返回不同的 JSON 响应,自定义中间件是最佳选择。

创建自定义中间件:

public class CustomExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<CustomExceptionHandlerMiddleware> _logger;

    public CustomExceptionHandlerMiddleware(RequestDelegate next, ILogger<CustomExceptionHandlerMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception occurred in custom middleware.");

            context.Response.ContentType = "application/json";
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            var errorResponse = new
            {
                Message = "服务器内部错误,请稍后再试。",
                // 生产环境不建议直接暴露异常信息
                DeveloperMessage = ex.Message
            };

            await context.Response.WriteAsJsonAsync(errorResponse);
        }
    }
}

注册自定义中间件:

在 Program.cs 里在其他中间件之前注册:

app.UseMiddleware<CustomExceptionHandlerMiddleware>();

// 其他中间件...

4. 全局异常过滤器 (Global Exception Filters)

全局异常过滤器是 ASP.NET Core MVC (和 Razor Pages) 中一种特殊的过滤器,用于处理在 控制器 (Controller) 或 Action 方法 执行过程中发生的未处理异常。它们提供了一个集中的位置来捕获这些异常,并允许你执行一些自定义逻辑。

假设我们有一个 Web 应用程序,并且我们希望在控制器/Action 发生异常时,统一记录日志并返回一个友好的错误响应。

创建自定义异常过滤器类 (CustomExceptionFilter.cs)

// CustomExceptionFilter.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using System.Net;

public class CustomExceptionFilter : IExceptionFilter
{
    private readonly ILogger<CustomExceptionFilter> _logger;

    public CustomExceptionFilter(ILogger<CustomExceptionFilter> logger)
    {
        _logger = logger;
    }

    public void OnException(ExceptionContext context)
    {
        // 记录异常信息
        _logger.LogError(context.Exception, "A global exception occurred in action/controller: {Path}", context.HttpContext.Request.Path);

        // 设置响应状态码
        context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        // 根据请求类型返回不同的响应
        // 如果是API请求 (例如, Accept: application/json 或 X-Requested-With: XMLHttpRequest)
        if (context.HttpContext.Request.Headers["Accept"].Contains("application/json") ||
            context.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
        {
            context.Result = new JsonResult(new
            {
                Status = StatusCodes.Status500InternalServerError,
                Message = "服务器内部错误,请稍后再试。",
                // 在开发环境可以考虑暴露更多信息,生产环境不建议
                // DevelopmentDetails = context.Exception.Message
            });
            context.HttpContext.Response.ContentType = "application/json";
        }
        else // 对于网页请求,重定向到错误页面或显示一个通用错误信息
        {
            // 这里可以重定向到一个统一的错误页面
            context.Result = new RedirectToActionResult("Error", "Home", new { statusCode = StatusCodes.Status500InternalServerError });
            // 或者直接返回一个 ViewResult
            // context.Result = new ViewResult { ViewName = "Error", ViewData = new ViewDataDictionary(context.ModelState) { { "ErrorMessage", "抱歉,服务器开小差了,请稍后再试。" } } };
        }

        // 标记异常已处理,防止它继续向上冒泡到 UseExceptionHandler
        context.ExceptionHandled = true;
    }
}

在 Program.cs 中配置服务和中间件:

// 配置服务
builder.Services.AddControllersWithViews(options =>
{
    // 将 CustomExceptionFilter 作为全局过滤器添加
    // 注意:这里使用 Add<TFilterType>() 是将过滤器注册为服务,
    // 因此 CustomExceptionFilter 可以通过依赖注入获取 ILogger
    options.Filters.Add<CustomExceptionFilter>();
});

全局异常过滤器适用于处理控制器层面的异常,可以方便地根据请求类型(同步请求、AJAX 请求)返回不同的结果。

总结与最佳实践

ASP.NET Core 提供了多层次的异常处理机制,让开发者能够应对各种错误场景。在实际开发中,建议采用以下最佳实践:

  • 开发环境使用 UseDeveloperExceptionPage(): 这可以提供详细的错误信息,便于调试。
  • 生产环境使用 UseExceptionHandler(): 确保应用在生产环境中能够优雅地处理未捕获异常,并向用户展示友好的错误页面。
  • 结合 UseStatusCodePagesWithReExecute() 处理 404 及其他状态码: 统一错误处理入口,提升用户体验并有利于 SEO。
  • 适当使用自定义中间件或全局异常过滤器: 对于复杂的业务异常处理、日志记录或根据请求类型返回不同响应的场景,它们提供了更细粒度的控制。
  • 日志记录是关键: 无论采用哪种异常处理方式,务必将异常信息记录到可靠的日志系统(如 Serilog, NLog, Application Insights),以便后续排查和解决问题。
  • 避免向用户暴露敏感信息: 在生产环境中,错误页面或错误响应中不应包含敏感的堆栈跟踪信息或内部错误细节。
  • 用户友好的错误信息: 错误提示应简洁明了,并引导用户进行下一步操作(例如“请稍后再试”或“联系管理员”)。

通过合理配置和利用 ASP.NET Core 的全局异常处理功能,可以显著提升应用的健壮性、可靠性,并为用户提供更流畅、更友好的体验。告别崩溃,从现在开始,让你的 ASP.NET Core 应用优雅地处理每一个异常!

评论