问题详情(Problem Details)是一种在 HTTP 响应中传递错误信息的标准方式,定义在 RFC 7807 中。标准的问题详情属性包括:
问题详情已自动集成到 .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"
}
我们可以通过异常处理中间件在单一位置捕获所有异常。
首先,创建用于处理 BadRequest
和 NotFoundException
的自定义异常类。
// 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 中的错误,并提供更丰富的错误信息。希望这篇文章对您有所帮助!如果觉得有用,请点赞并分享!