翻译 - ASP.NET Core 基本知识 - 错误处理(Handle errors)


翻译自 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-5.0

这篇文章涵盖了在 ASP.NET Core web 应用程序中处理错误的通用方法。查看  Handle errors in ASP.NET Core web APIs web APIs 信息。

开发者异常页面

开发者异常页面显示了关于没有处理的请求的异常的详细信息。ASP.NET Core 模板生成了以下代码:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

上面高亮的代码在应用程序运行在开发环境(Development environment)时启用了开发者异常页面。

模板把 UseDeveloperExceptionPage  放在了中间件管道靠上的地方,这样的话它可以捕获之后的中间件未处理的异常。

上面的代码仅仅在应用程序运行在开发环境时启用了开发者异常页面。详细的异常信息不应在生产环境中运行时公开显示。更多关于配置环境的信息,查看 Use multiple environments in ASP.NET Core。

开发者异常页面包括关于异常和请求的信息:

  • Stack trace
  • Query string parameters if any
  • Cookies if any
  • Headers

异常处理页面

配置生产环境(Production environment)自定义的错误处理页面,可以调用 UseExceptionHandler。这个异常处理中间件:

  • 捕获和记录未处理的异常
  • 使用指示的路径在备用的管道中重新执行请求。如果响应已经启动,请求不再重复执行。模板生成的代码使用 /Error 重新执行请求

在下面的例子中,UseExceptionHandler 在飞开发环境中添加异常处理中间件:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

 Razor Pages 应用程序模板提供一个错误页面(.cshtml)和 PageModel 类(ErrorModel)在 Pages 文件夹中。对于 MVC 应用程序,项目模板包含一个 Error 方法和一个 Error 视图在 Home 控制器中。

异常处理中间件会使用 original HTTP 方法重新执行请求。如果一个错误处理的 endpoint 被限制到一组特定的 HTTP 方法,它仅仅会运行那些 HTTP 方法。例如,一个 MVC 控制器方法使用 [HttpGet] 属性标记它仅仅运行 GET 请求。为了保证所有请求都能够访问到错误处理页面,不要限制它们到一组指定的 HTTP 方法。

处理异常的不同基于最初的 HTTP 方法:

  • 对于 Razor Pages,创建多个处理方法。例如,使用 OnGet 去处理 GET 异常,使用 OnPost 去处理 POST 异常。
  • 对于 MVC,应用 HTTP 动词属性到多个方法。例如,使用 [HttpGet] 属性处理 GET 异常,使用 [HttpPost] 处理 POST 异常

如果要允许没有认证的用户浏览自定义的错误处理页面,要保证它支持匿名访问。

获取异常

在一个错误处理中使用 IExceptionHandlerPathFeature 获取异常和原始的请求路径。下面的代码添加 ExceptionMessage 到 ASP.NET Core 模板生成的默认的 Pages/Error.cshtml 中:

[ResponseCache(Duration=0, Location=ResponseCacheLocation.None, NoStore=true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
    public string ExceptionMessage { get; set; }
    private readonly ILogger _logger;

    public ErrorModel(ILogger logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
        HttpContext.Features.Get();
        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "File error thrown";
            _logger.LogError(ExceptionMessage);
        }
        if (exceptionHandlerPathFeature?.Path == "/index")
        {
            ExceptionMessage += " from home page";
        }
    }
}

注意:

不要提供敏感的错误信息到客户端。提供错误信息是一个安全隐患。

测试示例应用程序(sample app)异常:

  • 设置环境为生产环境
  • 在 Program.cs 中,移除 webBuilder.UseStartup() 中的注释
  • 在 Home 页面选择 Tigger an exception

异常处理 lambda

自定义异常处理页面 (custom exception handler page) 的一种替代方案是给 UseExceptionHandler 提供一个 lambda。使用 lambda 允许在返回响应之前获取错误信息。

下面的代码使用了一个 lambda 来处理异常:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler(errorApp =>
        {
            errorApp.Run(async context =>
            {
                context.Response.StatusCode = 500;
                context.Response.ContentType = "text/html";

                await context.Response.WriteAsync("\r\n");
                await context.Response.WriteAsync("ERROR!

\r\n
"); var exceptionHandlerPathFeature = context.Features.Get(); if (exceptionHandlerPathFeature?.Error is FileNotFoundException) { await context.Response.WriteAsync( "File error thrown!

\r\n
"); } await context.Response.WriteAsync( "Home
\r\n
"); await context.Response.WriteAsync("\r\n"); await context.Response.WriteAsync(new string(' ', 512)); }); }); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); }

注意:

不要从 IExceptionHandlerFeature 或者 IExceptionHandlerPathFeature 提供敏感的错误信息到客户端。提供错误是存在安全风险的。

要测试示例程序( sample app)中的异常处理 lambda:

  • 设置环境为生产环境
  • 在 Program.cs 中,移除 webBuilder.UseStartup();的注释
  • 在 home 页面选择 Trigger an exception

UseStatusCodePages

默认的,ASP.NET Core 应用程序不会为 HTTP 错误状态码提供一个状态码页面,例如 404 - Not Found。当应用程序遇到一个没有 body 的 400-599 错误状态码时,它会返回状态码和一个空的响应 body。要提供一个状态码页面,使用状态码页面中间件。要使能为通常的错误状态码 text-only 处理的话,在 Startup.Config 中调用 UseStatusCodePages:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages();

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

在请求处理中间件之前调用 UseStatusCodePages。例如,在 Static File Middleware 和 Endpoints 中间件之前调用 UseStatusCodePages。

当没有使用 UseStatusCodePages 时,导航到一个没有 endpoint 的 URL 会返回一个依赖于浏览器的错误信息,表明 endpoint 不能被找到。例如,导航到 Home/Privacy2。当 UseStatusCodePages 被调用了,浏览器返回:

Status Code: 404; Not Found

一般的 UserStatusCodePages 在生产环境中不会用到,因为它返回的消息对用户没有什么用处。

测试示例程序中的 UseStatusCodePags:

  • 设置环境为生产
  • 移除 Program.cs 中 webBuilder.UserStartup(); 注释
  • 选择 home 页面上的链接

注意:

status code pages middleware 不会捕获异常。如要提供一个自定义的错误处理页面,使用 exception handler page。

带有格式化字符串的 UseStatusCodePages

为了自定义响应内容的类型和文本,使用 UseStatusCodePages 的带有内容类型和格式化字符串的重载方法:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages(
        "text/plain", "Status code page, status code: {0}");

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

在上面的代码中,{0} 是错误代码的占位符。

带有格式化字符串的 UseStatusCodePages 一般不在生产环境中使用,因为它返回的信息对用户来说没有用。

移除 Program.cs 中的 webBuilder.UseStartup(); 注释测试 UseStatusCodePages。

带有 lambda 的 UseStatusCodePages

要指定一个自定义的 error-handing 和 response-writing 代码,使用 UseStatusCodePages 带有一个 lambda 表达式的重载方法:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages(async context =>
    {
        context.HttpContext.Response.ContentType = "text/plain";

        await context.HttpContext.Response.WriteAsync(
            "Status code page, status code: " +
            context.HttpContext.Response.StatusCode);
    });

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

代码 lambda 参数的 UseStatusCodePages 的方法在生产环境中一般不会使用,因为它返回的信息对用户没有用。

移除 Program.cs 中 webBuilder.UseStartup(); 的注释测试 UseStatusCodePages。

UseStatusCodePagesWithRedirects

UseStatusCodePagesWithRedirects 扩展方法:

  • 发送一个  302 - Found 的状态码到客户端
  • 重定向客户端到 URL 模板中提供的错误处理的 endpoint。错误处理 endpoint 一般会显示错误信息并且返回 HTTP 200。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

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

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

URL 模板可以包含一个状态码 {0} 占位符,就像上面的代码。如果 URL 模板是以 ~ 开头,~ 会使用应用程序的 PathBase 替换掉。当在应用程序中指定一个 endpoint 时,会为 endpoint 创建一个 MVC 视图或者 Razor page。Razor Pages 的示例,查看  sample app 中的 Pages/MyStatusCode.cshtml。

这个方法通常在以下情况会在应用程序中用到:

  • 通常在一个不同应用程序处理错误的情况下,应该重定向客户端到不同的 endpoint。对于 web 应用程序,客户端的浏览器地址栏反映了重定向后的 endpoint。
  •  不应保留和返回带有最初重定向响应的原始状态码

移除 Program.cs 中 webBuilder.UseStartup(); 的注释测试 UseStartupCodePage。

UseStatusCodePagesWithReExecute

UseStatusCodePagesWithReExecute 扩展方法:

  • 返回原始的状态码到客户端
  • 通过重新执行使用可以使用的路径的请求管道生成响应体
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePagesWithReExecute("/MyStatusCode2", "?code={0}");

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

如果应用程序中的一个 endpoint 被指定,创建一个 MVC 视图或者 Razor 页面。保证 UseStatusCodePagesWithReExecute 放置到 UseRouting 之前,这样的话请求就可以被重新路由到状态页面。Razor Pages 的例子,查看 sample app 中的 Pages/MyStatusCode2.cshtml。

这个方法通常在应用程序在以下情况时:

  • 处理没有重定向到不同 endpoint 的请求。对于 web 应用程序,客户端的浏览器地址栏反映了原始的请求 endpoint
  • 保留和返回使用 response 的原始状态码

URL 和 query 字符串模板可能包含一个状态码的占位符 {0}。URL 模板必须以 / 开头。

处理错误的 endpoint 可以获取到产生错误的原始的 URL,就像下面的例子中一样:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class MyStatusCode2Model : PageModel
{
    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string ErrorStatusCode { get; set; }

    public string OriginalURL { get; set; }
    public bool ShowOriginalURL => !string.IsNullOrEmpty(OriginalURL);

    public void OnGet(string code)
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
        ErrorStatusCode = code;

        var statusCodeReExecuteFeature = HttpContext.Features.Get<
                                               IStatusCodeReExecuteFeature>();
        if (statusCodeReExecuteFeature != null)
        {
            OriginalURL =
                statusCodeReExecuteFeature.OriginalPathBase
                + statusCodeReExecuteFeature.OriginalPath
                + statusCodeReExecuteFeature.OriginalQueryString;
        }
    }
}

对于 Razor Page 的示例,查看  sample app 中的  Pages/MyStatusCode2.cshtml。

移除 Program.cs 中的 webBuilder.UseStartup(); 注释来测试 UseStatusCodePages。

禁用状态码页面

要为 MVC 控制器或者方法禁用状态码页面,可以使用 [SkipStatusCodePages] 属性。

要在 Razor Pages 中的处理方法中或者 MVC 控制器中为指定请求禁用状态码页面,可以使用 IStatusCodePagesFeature:

public void OnGet()
{
    // using Microsoft.AspNetCore.Diagnostics;
    var statusCodePagesFeature = HttpContext.Features.Get();

    if (statusCodePagesFeature != null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

异常处理代码

异常处理页面中的代码也可能会抛出异常。生产环境的错误页面应该彻底的测试并且要格外的小心避免抛出异常。

响应头

一旦响应的头部被发送:

  • 应用程序不能改变响应状态码
  • 任何异常页面或者处理都不能运行。响应必须完成或者连接终止

服务器异常处理

除了应用程序中的逻辑异常处理,HTTP server implementation  可以处理一些异常。如果服务器在响应头发送之前捕获了一个异常,服务器发送一个 500 - Internal Server Error 响应,没有响应体。如果服务器在响应头部发送之后捕获到一个异常,服务器会关闭连接。应用程序没有处理的请求会被服务器处理。任何当服务器正在处理请求的时候出现的异常都会被服务器异常处理。应用程序自定义的错误页面,异常处理中间件,和过滤器不会影响这个行为。

启动异常处理

只有主机层可以处理在应用程序启动时发生的异常。主机可以配置为 capture startup errors 和 capture detailed errors。

主机层只有在错误发生在主机地址/端口绑定后出现的错误被捕获到的错误才会显示错误页面。如果绑定失败了:

  • 主机层记录一个严格的异常
  • dotnet 进程崩溃
  • 当 HTTP 服务器是 Kestrel 时,没有错误页面会显式

当运行在 IIS (或者 Azure App Service) 或者 IIS Express 上时,一个 502.5 - Process Failure 在进程不能启动时通过 ASP.NET Core Module 返回。更多信息,查看 Troubleshoot ASP.NET Core on Azure App Service and IIS。

数据库错误页面

数据库开发者页面异常过滤 AddDatabaseDeveloperPageExceptionFilter 捕获数据库相关的异常,这些异常可以使用 Entity Framework Core 迁移解析。当这些异常出现时, 会生成带有可能的方法解决问题的详细信息的 HTML 被生成返回。这个页面仅仅在开发环境中会被启用。下面有 ASP.NET  Core Razor Pages 模板在私有用户账号被指定时生成的代码:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDatabaseDeveloperPageExceptionFilter();
    services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores();
    services.AddRazorPages();
}

异常过滤

在 MVC 应用程序中,异常过滤可以全局配置或者基于控制器或者方法。在 Razor Page 应用程序中,可以全局的或者 page model 配置。这个过滤器处理任何的发生在一个控制器方法或者另外的过滤器中未处理的异常。更多信息,查看 Filters in ASP.NET Core。

异常过滤器对于捕获出现在 MVC actions 中的异常非常有用,但是它们没有内置的  exception handling middleware 更加灵活,UseExceptionHandler。我们建议使用 UseExceptionHandler,除非你需要处理基于不哪个 MVC action 被选择时处理不同的错误。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

模型状态错误

关于如何处理模型状态错误,查看 Model binding 和 Model validation。