深入解析 .NET Core 中的问题详情与全局异常处理:打造更优雅的 API 错误响应

作者:微信公众号:【架构师老卢】
3-2 9:33
14

问题详情(Problem Details)简介

问题详情(Problem Details)是一种在 HTTP 响应中传递错误信息的标准方式,定义在 RFC 7807 中。标准的问题详情属性包括:

  • Type:标识问题类型的 URI
  • Title:简短的错误描述
  • Status:HTTP 状态码
  • Detail:具体的错误解释
  • Instance:标识特定错误发生的 URI

问题详情已自动集成到 .NET Core API 中。当我们返回 BadRequest 时,通常会得到包含问题详情的响应。


默认行为示例

返回 BadRequest

// 控制器方法
return BadRequest();

响应:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "traceId": "00-2d4948694b0f223f7f5dff215b42481b-0288bb95d7604783-00"
}

返回 NotFoundException

// 控制器方法
return NotFound();

响应:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404,
  "traceId": "00-665f3aa493eea5f307292a5862fca17e-790e01fa0f9386df-00"
}

验证错误消息

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Name": [
      "The Name field is required."
    ]
  },
  "traceId": "00-75969d38c366a25c50297e96ec3bc265-231440661829261c-00"
}

自定义问题详情

如果我们希望在 NotFound() 中传递一些消息,默认行为会有所不同。

示例

// 控制器方法
return NotFound("Person not found");

响应:

Person not found

在这种情况下,响应仅包含 404 状态码和传递给 NotFound() 方法的字符串内容,而不是问题详情格式。

使用 Problem() 方法

我们可以使用 Problem() 方法来解决这个问题,从而自定义问题详情。

// 请求
return Problem(
    type: "Not found exception", 
    title: "An error is occured", 
    detail: "User does not found", 
    statusCode: 404);

响应:

{
  "type": "Not found exception",
  "title": "An error is occured",
  "status": 404,
  "detail": "User does not found",
  "traceId": "00-1999d07fdaddf513f0cc4ea9244a4cd2-beb18ed447ecdb65-00"
}

添加更多详细信息

我们可以通过配置 Program 类来向问题详情响应中添加更多详细信息。

// Program.cs
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Instance = $"{context.HttpContext.Request.Method} {context.HttpContext.Request.Path}";
        context.ProblemDetails.Extensions.TryAdd("requestId", context.HttpContext.TraceIdentifier);
        
        var activity = context.HttpContext.Features.Get<IHttpActivityFeature>()?.Activity;
        context.ProblemDetails.Extensions.TryAdd("traceId", activity.Id);
    };
});

现在,我们的响应将如下所示:

{
  "type": "Not found exception",
  "title": "An error is occured",
  "status": 400,
  "detail": "User does not found",
  "instance": "GET /api/greetings",
  "traceId": "00-0b258efbf453b2ab17ae347f28200faf-9f2c4c1177edb3ae-00",
  "requestId": "0HN8AV48Q51I4:00000001"
}

全局异常处理

我们可以通过异常处理中间件在单一位置捕获所有异常。

自定义异常类

首先,创建用于处理 BadRequestNotFoundException 的自定义异常类。

// BadRequestException.cs
public class BadRequestException : Exception
{
    public BadRequestException(string message):base(message)
    {
    }
}

// NotFoundException.cs
public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message)
    {
    }
}

创建异常处理器

创建一个名为 CustomExceptionHandler 的类。

public class CustomExceptionHandler: IExceptionHandler
{
    private readonly IProblemDetailsService _problemDetailService;
    private readonly ILogger<CustomExceptionHandler> _logger;

    public CustomExceptionHandler(IProblemDetailsService problemDetailService, ILogger<CustomExceptionHandler> logger)
    {
        _problemDetailService = problemDetailService;
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        _logger.LogError(exception.Message);

        // 根据异常确定状态码
        var (statusCode, problemDetails) = GetProblemDetailsAndStatusCode(exception);

        httpContext.Response.StatusCode = statusCode;

        return await _problemDetailService.TryWriteAsync(new ProblemDetailsContext {
            HttpContext = httpContext,
            ProblemDetails = problemDetails,
            Exception = exception
        });
    }

    private (int, ProblemDetails) GetProblemDetailsAndStatusCode(Exception exception)
    {
        return exception switch
        {
            BadRequestException => 
            (
                StatusCodes.Status400BadRequest,
                new ProblemDetails
                {
                    Status = StatusCodes.Status400BadRequest,
                    Title = "Bad request",
                    Detail = exception.Message,
                    Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
                }
            ),
            NotFoundException => (
                StatusCodes.Status404NotFound,
                new ProblemDetails
                {
                    Status = StatusCodes.Status404NotFound,
                    Title = "Resource not found",
                    Detail = exception.Message,
                    Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"
                }
            ),
            _ => (
                StatusCodes.Status500InternalServerError,
                new ProblemDetails
                {
                    Status = StatusCodes.Status500InternalServerError,
                    Title = "Server error",
                    Detail = exception.Message,
                    Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1"
                }
            ),
        };
    }
}

注册异常处理中间件

Program.cs 中注册异常处理中间件。

// Program.cs
builder.Services.AddExceptionHandler<CustomExceptionHandler>();
app.UseExceptionHandler();

最终的 Program 类如下:

using Microsoft.AspNetCore.Http.Features;

var builder = WebApplication.CreateBuilder(args);

// 添加服务到容器
builder.Services.AddControllers();
builder.Services.AddOpenApi();

// 添加问题详情配置
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Instance = $"{context.HttpContext.Request.Method} {context.HttpContext.Request.Path}";
        context.ProblemDetails.Extensions.TryAdd("requestId", context.HttpContext.TraceIdentifier);
        var activity = context.HttpContext.Features.Get<IHttpActivityFeature>()?.Activity;
        context.ProblemDetails.Extensions.TryAdd("traceId", activity.Id);
    };
});

// 注册异常处理器
builder.Services.AddExceptionHandler<CustomExceptionHandler>();

var app = builder.Build();

// 配置 HTTP 请求管道
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseExceptionHandler();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

测试自定义异常

抛出自定义异常并检查响应。

// 控制器方法
throw new NotFoundException("User does not found");

响应:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
  "title": "Resource not found",
  "status": 404,
  "detail": "User does not found",
  "instance": "GET /api/greetings",
  "traceId": "00-f4d3214afd423ad8d13d934062b283d7-f773987abae78043-00",
  "requestId": "0HN8BHUP5M9SI:00000001"
}

通过使用问题详情和全局异常处理,我们可以更优雅地处理 API 中的错误,并提供更丰富的错误信息。希望这篇文章对您有所帮助!如果觉得有用,请点赞并分享!

相关留言评论
昵称:
邮箱:
阅读排行