在基于 ASP.NET Core 控制器的 Web API 中处理错误

注意

此版本不是本文的最新版本。 有关当前版本,请参阅本文.NET 9 版本。

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

有关当前版本,请参阅本文.NET 9 版本。

本文介绍如何在基于控制器的 ASP.NET Core Web API 中处理错误以及自定义错误处理。 有关最小 API 中的错误处理的信息,请参阅在 ASP.NET Core 中处理错误在最小 API 中处理错误

开发人员异常页

“开发人员异常”页显示未经处理的请求异常的详细信息。 它使用 DeveloperExceptionPageMiddleware 来捕获 HTTP 管道中的同步和异步异常,并生成错误响应。 开发人员异常页运行在中间件管道的前面部分,以便它能够捕获随后中间件中抛出的未经处理的异常。

ASP.NET Core 应用在以下情况下默认启用开发人员异常页:

使用早期模板(即使用 WebHost.CreateDefaultBuilder)创建的应用可以通过调用 app.UseDeveloperExceptionPage 来启用开发人员异常页。

警告

仅当应用在开发环境中运行时才启用“开发人员异常页”。 当应用在生产环境中运行时,切勿公开共享详细的异常信息。 有关配置环境的详细信息,请参阅在 ASP.NET Core 中使用多个环境

开发人员异常页可能包括关于异常和请求的以下信息:

  • 堆栈跟踪
  • 查询字符串参数(如果有)
  • Cookie(如有)
  • 标头
  • 终结点元数据(如果有)

不保证开发人员异常页会提供任何信息。 使用日志记录获取完整的错误信息。

下图显示了一个示例开发人员异常页面,通过动画来显示选项卡和展示的信息:

开发人员异常页面以动画形式显示所选的每个选项卡。

在响应带有 Accept: text/plain 标头的请求时,开发人员异常页将返回纯文本而不是 HTML。 例如:

Status: 500 Internal Server Error
Time: 9.39 msSize: 480 bytes
FormattedRawHeadersRequest
Body
text/plain; charset=utf-8, 480 bytes
System.InvalidOperationException: Sample Exception
   at WebApplicationMinimal.Program.<>c.<Main>b__0_0() in C:\Source\WebApplicationMinimal\Program.cs:line 12
   at lambda_method1(Closure, Object, HttpContext)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

HEADERS
=======
Accept: text/plain
Host: localhost:7267
traceparent: 00-0eab195ea19d07b90a46cd7d6bf2f

若要查看开发人员异常页,请执行以下操作:

  • 将以下控制器操作添加到基于控制器的 API。 请求终结点时,该操作会引发异常。

    [HttpGet("Throw")]
    public IActionResult Throw() =>
        throw new Exception("Sample exception.");
    
  • 开发环境中运行应用。

  • 转到控制器操作定义的终结点。

异常处理程序

在非开发环境中,使用异常处理中间件来生成错误有效负载:

  1. Program.cs 中,调用 UseExceptionHandler 来添加异常处理中间件:

    var app = builder.Build();
    
    app.UseHttpsRedirection();
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error");
    }
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    
  2. 配置控制器操作以响应 /error 路由:

    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

前面的 HandleError 操作向客户端发送符合 RFC 7807 的负载。

警告

不要使用 HTTP 方法属性(如 HttpGet)标记错误处理程序操作方法。 显式谓词可阻止某些请求访问操作方法。

对于使用 Swagger / OpenAPI 的 Web API,请使用 [ApiExplorerSettings] 属性来标记错误处理程序操作,并将它的 IgnoreApi 属性设置为 true。 此属性配置将错误处理程序操作排除在应用的 OpenAPI 规范外:

[ApiExplorerSettings(IgnoreApi = true)]

如果要使未经身份验证的用户能够看到错误,则允许匿名访问该方法。

还可以在开发环境中使用异常处理中间件来生成所有环境中一致的有效负载格式:

  1. Program.cs 中,注册特定于环境的异常处理中间件实例:

    if (app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error-development");
    }
    else
    {
        app.UseExceptionHandler("/error");
    }
    

    在上述代码中,通过以下方法注册了中间件:

    • 开发环境中 /error-development 的路由。
    • 非开发环境中 /error 的路由。

  2. 为开发和非开发路由添加控制器操作:

    [Route("/error-development")]
    public IActionResult HandleErrorDevelopment(
        [FromServices] IHostEnvironment hostEnvironment)
    {
        if (!hostEnvironment.IsDevelopment())
        {
            return NotFound();
        }
    
        var exceptionHandlerFeature =
            HttpContext.Features.Get<IExceptionHandlerFeature>()!;
    
        return Problem(
            detail: exceptionHandlerFeature.Error.StackTrace,
            title: exceptionHandlerFeature.Error.Message);
    }
    
    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

使用异常来修改响应

可以使用自定义异常和操作筛选器从控制器外部修改响应的内容:

  1. 创建名为 HttpResponseException 的已知异常类型:

    public class HttpResponseException : Exception
    {
        public HttpResponseException(int statusCode, object? value = null) =>
            (StatusCode, Value) = (statusCode, value);
    
        public int StatusCode { get; }
    
        public object? Value { get; }
    }
    
  2. 创建名为 HttpResponseExceptionFilter 的操作筛选器:

    public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        public int Order => int.MaxValue - 10;
    
        public void OnActionExecuting(ActionExecutingContext context) { }
    
        public void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Exception is HttpResponseException httpResponseException)
            {
                context.Result = new ObjectResult(httpResponseException.Value)
                {
                    StatusCode = httpResponseException.StatusCode
                };
    
                context.ExceptionHandled = true;
            }
        }
    }
    

    上述筛选器指定最大整数值减 10 的 Order。 此 Order 允许其他筛选器在管道结束时运行。

  3. Program.cs 中,将操作筛选器添加到筛选器集合:

    builder.Services.AddControllers(options =>
    {
        options.Filters.Add<HttpResponseExceptionFilter>();
    });
    

验证失败错误响应

对于 Web API 控制器,如果模型验证失败,MVC 将使用 ValidationProblemDetails 响应类型做出响应。 MVC 使用 InvalidModelStateResponseFactory 的结果来构造验证失败的错误响应。 在 Program.cs 中,以下示例使用也支持将响应格式化为 XML 的实现替代了默认工厂:

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
            new BadRequestObjectResult(context.ModelState)
            {
                ContentTypes =
                {
                    // using static System.Net.Mime.MediaTypeNames;
                    Application.Json,
                    Application.Xml
                }
            };
    })
    .AddXmlSerializerFormatters();

客户端错误响应

错误结果定义为具有 HTTP 状态代码 400 或更高的结果。 对于 Web API 控制器,MVC 会转换错误结果来生成 ProblemDetails

默认情况下,会自动为错误状态代码创建 ProblemDetails,但可以通过以下方式之一配置错误响应:

  1. 使用问题详细信息服务
  2. 实现 ProblemDetailsFactory
  3. 使用 ApiBehaviorOptions.ClientErrorMapping

默认问题详细信息响应

以下 Program.cs 文件由 API 控制器的 Web 应用程序模板生成:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

请考虑以下控制器,它在输入无效时返回 BadRequest

[Route("api/[controller]/[action]")]
[ApiController]
public class Values2Controller : ControllerBase
{
    // /api/values2/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values2 /squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }
}

以下任一条件适用时,会使用前面的代码生成问题详细信息响应:

  • /api/values2/divide 终结点是通过零分母调用的。
  • /api/values2/squareroot 终结点是通过小于零的被开方数调用的。

默认问题详细信息响应正文具有以下 typetitlestatus 值:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "Bad Request",
  "status": 400,
  "traceId": "00-84c1fd4063c38d9f3900d06e56542d48-85d1d4-00"
}

问题详细信息服务

ASP.NET Core 支持使用 IProblemDetailsService为 HTTP API 创建问题详细信息。 有关详细信息,请参阅问题详细信息服务

以下代码将应用配置为为所有 尚未包含正文内容的 HTTP 客户端和服务器错误响应生成问题详细信息响应

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();

请考虑上一部分中的 API 控制器,其在输入无效时返回 BadRequest

[Route("api/[controller]/[action]")]
[ApiController]
public class Values2Controller : ControllerBase
{
    // /api/values2/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values2 /squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }
}

以下任一条件适用时,会使用前面的代码生成问题详细信息响应:

  • 提供的输入无效。
  • URI 没有匹配的终结点。
  • 发生未经处理的异常。

SuppressMapClientErrors 属性设置为 true 时,会禁止自动创建错误状态代码的 ProblemDetails

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressMapClientErrors = true;
    });

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

使用上述代码,当 API 控制器返回 BadRequest 时,将返回 HTTP 400 响应状态,而没有响应正文。 SuppressMapClientErrors 防止创建 ProblemDetails 响应,即使在为 API 控制器终结点调用 WriteAsync 时也是如此。 本文后面将介绍 WriteAsync

下一部分介绍如何使用 CustomizeProblemDetails 自定义问题详细信息响应正文,以返回更有用的响应。 有关更多自定义选项,请参阅自定义问题详细信息

使用 CustomizeProblemDetails 自定义问题详细信息

以下代码使用 ProblemDetailsOptions 设置 CustomizeProblemDetails

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails(options =>
        options.CustomizeProblemDetails = (context) =>
        {

            var mathErrorFeature = context.HttpContext.Features
                                                       .Get<MathErrorFeature>();
            if (mathErrorFeature is not null)
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError =>
                    ("Divison by zero is not defined.",
                                          "https://wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                                          "https://wikipedia.org/wiki/Square_root")
                };

                context.ProblemDetails.Type = details.Type;
                context.ProblemDetails.Title = "Bad Input";
                context.ProblemDetails.Detail = details.Detail;
            }
        }
    );

var app = builder.Build();

app.UseHttpsRedirection();

app.UseStatusCodePages();

app.UseAuthorization();

app.MapControllers();

app.Run();

更新的 API 控制器:

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}

以下代码包含与前面的示例一起使用的 MathErrorFeatureMathErrorType

// Custom Http Request Feature
class MathErrorFeature
{
    public MathErrorType MathError { get; set; }
}

// Custom math errors
enum MathErrorType
{
    DivisionByZeroError,
    NegativeRadicandError
}

以下任一条件适用时,会使用前面的代码生成问题详细信息响应:

  • /divide 终结点是通过零分母调用的。
  • /squareroot 终结点是通过小于零的被开方数调用的。
  • URI 没有匹配的终结点。

当通过小于零的被开方数调用任一 squareroot 终结点时,问题详细信息响应正文包含以下内容:

{
  "type": "https://en.wikipedia.org/wiki/Square_root",
  "title": "Bad Input",
  "status": 400,
  "detail": "Negative or complex numbers are not allowed."
}

查看或下载示例代码

实现 ProblemDetailsFactory

MVC 使用 Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory 生成 ProblemDetailsValidationProblemDetails 的所有实例。 此工厂用于:

若要自定义问题详细信息响应,请在 Program.cs 中注册 ProblemDetailsFactory 的自定义实现:

builder.Services.AddControllers();
builder.Services.AddTransient<ProblemDetailsFactory, SampleProblemDetailsFactory>();

使用 ApiBehaviorOptions.ClientErrorMapping

使用 ClientErrorMapping 属性配置 ProblemDetails 响应的内容。 例如,Program.cs 中的以下代码会更新 404 响应的 Link 属性:

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

其他资源

本文介绍如何处理 ASP.NET Core Web API 的错误以及自定义错误处理。

开发人员异常页

开发人员异常页显示服务器错误的详细堆栈跟踪。 它使用 DeveloperExceptionPageMiddleware 来捕获 HTTP 管道中的同步和异步异常,并生成错误响应。 例如,考虑以下控制器操作,该操作将引发异常:

[HttpGet("Throw")]
public IActionResult Throw() =>
    throw new Exception("Sample exception.");

当“开发人员异常页”检测到未处理的异常时,它会生成类似以下示例的默认纯文本响应:

HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

System.Exception: Sample exception.
   at HandleErrorsSample.Controllers.ErrorsController.Get() in ...
   at lambda_method1(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()

...

如果客户端请求 HTML 格式的响应,“开发人员异常页”将生成类似以下示例的响应:

HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="utf-8" />
        <title>Internal Server Error</title>
        <style>
            body {
    font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif;
    font-size: .813em;
    color: #222;
    background-color: #fff;
}

h1 {
    color: #44525e;
    margin: 15px 0 15px 0;
}

...

若要请求 HTML 格式的响应,请将 Accept HTTP 请求标头设置为 text/html

警告

仅当应用在开发环境中运行时才启用“开发人员异常页”。 当应用在生产环境中运行时,切勿公开共享详细的异常信息。 有关配置环境的详细信息,请参阅在 ASP.NET Core 中使用多个环境

异常处理程序

在非开发环境中,使用异常处理中间件来生成错误有效负载:

  1. Program.cs 中,调用 UseExceptionHandler 来添加异常处理中间件:

    var app = builder.Build();
    
    app.UseHttpsRedirection();
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error");
    }
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    
  2. 配置控制器操作以响应 /error 路由:

    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

前面的 HandleError 操作向客户端发送符合 RFC 7807 的负载。

警告

不要使用 HTTP 方法属性(如 HttpGet)标记错误处理程序操作方法。 显式谓词可阻止某些请求访问操作方法。

对于使用 Swagger / OpenAPI 的 Web API,请使用 [ApiExplorerSettings] 属性来标记错误处理程序操作,并将它的 IgnoreApi 属性设置为 true。 此属性配置将错误处理程序操作排除在应用的 OpenAPI 规范外:

[ApiExplorerSettings(IgnoreApi = true)]

如果要使未经身份验证的用户能够看到错误,则允许匿名访问该方法。

还可以在开发环境中使用异常处理中间件来生成所有环境中一致的有效负载格式:

  1. Program.cs 中,注册特定于环境的异常处理中间件实例:

    if (app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error-development");
    }
    else
    {
        app.UseExceptionHandler("/error");
    }
    

    在上述代码中,通过以下方法注册了中间件:

    • 开发环境中 /error-development 的路由。
    • 非开发环境中 /error 的路由。

  2. 为开发和非开发路由添加控制器操作:

    [Route("/error-development")]
    public IActionResult HandleErrorDevelopment(
        [FromServices] IHostEnvironment hostEnvironment)
    {
        if (!hostEnvironment.IsDevelopment())
        {
            return NotFound();
        }
    
        var exceptionHandlerFeature =
            HttpContext.Features.Get<IExceptionHandlerFeature>()!;
    
        return Problem(
            detail: exceptionHandlerFeature.Error.StackTrace,
            title: exceptionHandlerFeature.Error.Message);
    }
    
    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

使用异常来修改响应

可以使用自定义异常和操作筛选器从控制器外部修改响应的内容:

  1. 创建名为 HttpResponseException 的已知异常类型:

    public class HttpResponseException : Exception
    {
        public HttpResponseException(int statusCode, object? value = null) =>
            (StatusCode, Value) = (statusCode, value);
    
        public int StatusCode { get; }
    
        public object? Value { get; }
    }
    
  2. 创建名为 HttpResponseExceptionFilter 的操作筛选器:

    public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        public int Order => int.MaxValue - 10;
    
        public void OnActionExecuting(ActionExecutingContext context) { }
    
        public void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Exception is HttpResponseException httpResponseException)
            {
                context.Result = new ObjectResult(httpResponseException.Value)
                {
                    StatusCode = httpResponseException.StatusCode
                };
    
                context.ExceptionHandled = true;
            }
        }
    }
    

    上述筛选器指定最大整数值减 10 的 Order。 此 Order 允许其他筛选器在管道结束时运行。

  3. Program.cs 中,将操作筛选器添加到筛选器集合:

    builder.Services.AddControllers(options =>
    {
        options.Filters.Add<HttpResponseExceptionFilter>();
    });
    

验证失败错误响应

对于 Web API 控制器,如果模型验证失败,MVC 将使用 ValidationProblemDetails 响应类型做出响应。 MVC 使用 InvalidModelStateResponseFactory 的结果来构造验证失败的错误响应。 在 Program.cs 中,以下示例使用也支持将响应格式化为 XML 的实现替代了默认工厂:

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
            new BadRequestObjectResult(context.ModelState)
            {
                ContentTypes =
                {
                    // using static System.Net.Mime.MediaTypeNames;
                    Application.Json,
                    Application.Xml
                }
            };
    })
    .AddXmlSerializerFormatters();

客户端错误响应

错误结果定义为具有 HTTP 状态代码 400 或更高的结果。 对于 Web API 控制器,MVC 会转换错误结果来生成 ProblemDetails

可以通过以下方式之一配置错误响应:

  1. 实现 ProblemDetailsFactory
  2. 使用 ApiBehaviorOptions.ClientErrorMapping

实现 ProblemDetailsFactory

MVC 使用 Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory 生成 ProblemDetailsValidationProblemDetails 的所有实例。 此工厂用于:

若要自定义问题详细信息响应,请在 Program.cs 中注册 ProblemDetailsFactory 的自定义实现:

builder.Services.AddControllers();
builder.Services.AddTransient<ProblemDetailsFactory, SampleProblemDetailsFactory>();

使用 ApiBehaviorOptions.ClientErrorMapping

使用 ClientErrorMapping 属性配置 ProblemDetails 响应的内容。 例如,Program.cs 中的以下代码会更新 404 响应的 Link 属性:

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

用于处理异常的自定义中间件

异常处理中间件中的默认设置适用于大多数应用。 对于需要专用异常处理的应用,请考虑自定义异常处理中间件

为异常生成 ProblemDetails 有效负载

当发生未处理的异常时,ASP.NET Core 不会生成标准化的错误有效负载。 对于需要向客户端返回标准化 ProblemDetails 响应的场景,可以使用 ProblemDetails 中间件将异常和 404 响应映射到 ProblemDetails 有效负载。 异常处理中间件还可用于返回未处理的异常的 ProblemDetails 有效负载。

其他资源

本文介绍如何处理和自定义 ASP.NET Core Web API 的错误处理。

查看或下载示例代码如何下载

开发人员异常页

开发人员异常页是一种用于获取服务器错误详细堆栈跟踪的有用工具。 它使用 DeveloperExceptionPageMiddleware 来捕获 HTTP 管道中的同步和异步异常,并生成错误响应。 为了进行说明,请考虑以下控制器操作:

[HttpGet("{city}")]
public WeatherForecast Get(string city)
{
    if (!string.Equals(city?.TrimEnd(), "Redmond", StringComparison.OrdinalIgnoreCase))
    {
        throw new ArgumentException(
            $"We don't offer a weather forecast for {city}.", nameof(city));
    }
    
    return GetWeather().First();
}

运行以下 curl 命令以测试前面的操作:

curl -i https://localhost:5001/weatherforecast/chicago

如果客户端不请求 HTML 格式的输出,“开发人员异常页”将显示纯文本响应。 随即显示以下输出:

HTTP/1.1 500 Internal Server Error
Transfer-Encoding: chunked
Content-Type: text/plain
Server: Microsoft-IIS/10.0
X-Powered-By: ASP.NET
Date: Fri, 27 Sep 2019 16:13:16 GMT

System.ArgumentException: We don't offer a weather forecast for chicago. (Parameter 'city')
   at WebApiSample.Controllers.WeatherForecastController.Get(String city) in C:\working_folder\aspnet\AspNetCore.Docs\aspnetcore\web-api\handle-errors\samples\3.x\Controllers\WeatherForecastController.cs:line 34
   at lambda_method(Closure , Object , Object[] )
   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

HEADERS
=======
Accept: */*
Host: localhost:44312
User-Agent: curl/7.55.1

要改为显示 HTML 格式的响应,请将 Accept HTTP 请求头设置为 text/html 媒体类型。 例如:

curl -i -H "Accept: text/html" https://localhost:5001/weatherforecast/chicago

请考虑以下 HTTP 响应摘录:

HTTP/1.1 500 Internal Server Error
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/10.0
X-Powered-By: ASP.NET
Date: Fri, 27 Sep 2019 16:55:37 GMT

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="utf-8" />
        <title>Internal Server Error</title>
        <style>
            body {
    font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif;
    font-size: .813em;
    color: #222;
    background-color: #fff;
}

通过 curl 等工具进行测试时,HTML 格式的响应会很有用。

警告

仅当应用程序在开发环境中运行时才启用开发人员异常页。 当应用在生产环境中运行时,切勿公开共享详细的异常信息。 有关配置环境的详细信息,请参阅在 ASP.NET Core 中使用多个环境

不要使用 HTTP 方法属性(如 HttpGet)标记错误处理程序操作方法。 显式谓词可阻止某些请求访问操作方法。 如果要使未经身份验证的用户能够看到错误,则允许匿名访问该方法。

异常处理程序

在非开发环境中,可使用异常处理中间件来生成错误负载:

  1. Startup.Configure 中,调用 UseExceptionHandler 以使用中间件:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/error");
        }
    
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  2. 配置控制器操作以响应 /error 路由:

    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error")]
        public IActionResult Error() => Problem();
    }
    

前面的 Error 操作向客户端发送符合 RFC 7807 的负载。

异常处理中间件还可以在本地开发环境中提供更详细的内容协商输出。 使用以下步骤在开发和生产环境中生成一致的负载格式:

  1. Startup.Configure 中,注册特定于环境的异常处理中间件实例:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseExceptionHandler("/error-local-development");
        }
        else
        {
            app.UseExceptionHandler("/error");
        }
    }
    

    在上述代码中,通过以下方法注册了中间件:

    • 开发环境中 /error-local-development 的路由。
    • 非开发环境中 /error 的路由。

  2. 将属性路由应用于控制器操作:

    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error-local-development")]
        public IActionResult ErrorLocalDevelopment(
            [FromServices] IWebHostEnvironment webHostEnvironment)
        {
            if (webHostEnvironment.EnvironmentName != "Development")
            {
                throw new InvalidOperationException(
                    "This shouldn't be invoked in non-development environments.");
            }
    
            var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
    
            return Problem(
                detail: context.Error.StackTrace,
                title: context.Error.Message);
        }
    
        [Route("/error")]
        public IActionResult Error() => Problem();
    }
    

    上述代码调用 ControllerBase.Problem 来创建 ProblemDetails 响应。

使用异常来修改响应

可以从控制器之外修改响应的内容。 在 ASP.NET 4.x Web API 中,执行此操作的一种方法是使用 HttpResponseException 类型。 ASP.NET Core 不包括等效类型。 可以使用以下步骤来添加对 HttpResponseException 的支持:

  1. 创建名为 HttpResponseException 的已知异常类型:

    public class HttpResponseException : Exception
    {
        public int Status { get; set; } = 500;
    
        public object Value { get; set; }
    }
    
  2. 创建名为 HttpResponseExceptionFilter 的操作筛选器:

    public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        public int Order { get; } = int.MaxValue - 10;
    
        public void OnActionExecuting(ActionExecutingContext context) { }
    
        public void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Exception is HttpResponseException exception)
            {
                context.Result = new ObjectResult(exception.Value)
                {
                    StatusCode = exception.Status,
                };
                context.ExceptionHandled = true;
            }
        }
    }
    

    上述筛选器指定最大整数值减 10 的 Order。 此 Order 允许其他筛选器在管道结束时运行。

  3. Startup.ConfigureServices 中,将操作筛选器添加到筛选器集合:

    services.AddControllers(options =>
        options.Filters.Add(new HttpResponseExceptionFilter()));
    

验证失败错误响应

对于 Web API 控制器,如果模型验证失败,MVC 将使用 ValidationProblemDetails 响应类型做出响应。 MVC 使用 InvalidModelStateResponseFactory 的结果来构造验证失败的错误响应。 下面的示例使用工厂在 Startup.ConfigureServices 中将默认响应类型更改为 SerializableError

services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            var result = new BadRequestObjectResult(context.ModelState);

            // TODO: add `using System.Net.Mime;` to resolve MediaTypeNames
            result.ContentTypes.Add(MediaTypeNames.Application.Json);
            result.ContentTypes.Add(MediaTypeNames.Application.Xml);

            return result;
        };
    });

客户端错误响应

错误结果定义为具有 HTTP 状态代码 400 或更高的结果。 对于 Web API 控制器,MVC 会将错误结果转换为具有 ProblemDetails 的结果。

可以通过以下方式之一配置错误响应:

  1. 实现 ProblemDetailsFactory
  2. 使用 ApiBehaviorOptions.ClientErrorMapping

实现 ProblemDetailsFactory

MVC 使用 Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory 生成 ProblemDetailsValidationProblemDetails 的所有实例。 此工厂用于:

若要自定义问题详细信息响应,请在 Startup.ConfigureServices 中注册 ProblemDetailsFactory 的自定义实现:

public void ConfigureServices(IServiceCollection serviceCollection)
{
    services.AddControllers();
    services.AddTransient<ProblemDetailsFactory, CustomProblemDetailsFactory>();
}

使用 ApiBehaviorOptions.ClientErrorMapping

使用 ClientErrorMapping 属性配置 ProblemDetails 响应的内容。 例如,Startup.ConfigureServices 中的以下代码会更新 404 响应的 type 属性:

services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
        options.DisableImplicitFromServicesParameters = true;
    });

用于处理异常的自定义中间件

异常处理中间件中的默认设置适用于大多数应用。 对于需要专用异常处理的应用,请考虑自定义异常处理中间件

为异常生成 ProblemDetails 有效负载

当发生未处理的异常时,ASP.NET Core 不会生成标准化的错误有效负载。 对于需要向客户端返回标准化 ProblemDetails 响应的场景,可以使用 ProblemDetails 中间件将异常和 404 响应映射到 ProblemDetails 有效负载。 异常处理中间件还可用于返回未处理的异常的 ProblemDetails 有效负载。