应使用 ProblemDetails 类构造错误响应,因其轻量可靠、符合 RFC 7807 标准、自动设置 Content-Type 和状态码;避免用 BadRequest(new { message = "xxx" }) 等非标准写法,防止客户端无法解析关键字段、状态码错乱及敏感信息泄露。

直接用 ProblemDetails 类构造并返回,是最轻量、最可靠的方式——它不依赖第三方库,不需额外配置,且能确保 Content-Type 自动设为 application/problem+json,状态码也与 status 字段严格对齐。
为什么不能只写 return BadRequest(new { message = "xxx" })
这种写法绕过了 RFC 7807 标准,导致客户端无法按规范解析错误类型。常见后果包括:
- 前端用标准库(如 axios 的
isAxiosError或 OpenAPI 生成的 client)收不到type、title等关键字段 -
status字段被忽略,响应码固定为 400,哪怕你本意是返回 422(校验失败)或 409(冲突) - 生产环境可能意外暴露堆栈、路径、数据库名等敏感信息
正确做法是显式构造 ProblemDetails 实例,并用 ObjectResult 或 StatusCode 返回:
return new ObjectResult(new ProblemDetails
{
Type = "https://example.com/errors/validation-failed",
Title = "验证失败",
Status = 422,
Detail = "价格必须为正数",
Instance = Request.Path
});
UseExceptionHandler 中间件必须放对位置
它只在中间件管道中特定位置才真正生效,顺序错一点,404、路由解析失败、静态资源异常就全漏掉:
- 必须在
UseRouting()之后、UseEndpoints()之前调用 - 开发环境若同时启用了
UseDeveloperExceptionPage(),它会抢先拦截并返回 HTML,导致UseExceptionHandler完全不触发——上线前务必移除或用env.IsDevelopment()条件包裹 - 传入的错误处理路径(如
"/error")必须对应一个真实存在的控制器 Action,且该 Action 内不能再抛异常,否则会无限递归
推荐写法(.NET 6+ Program.cs):
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/error");
}
else
{
app.UseDeveloperExceptionPage();
}
自定义扩展字段时,别碰 type、title 等保留名
RFC 7807 允许添加任意扩展属性,但前提是不能覆盖标准字段。常见踩坑点:
- 继承
ProblemDetails后,加了public string type { get; set; }——这会遮蔽基类的Type属性,序列化后变成两个type字段,客户端解析失败 - 扩展字段名用了
status或detail(小写),和框架默认字段冲突,JSON 序列化器行为不可控 -
type字段为空或 null:客户端(尤其 Java/Swift 强类型解析器)会直接抛空指针,必须保证非空,建议用 URI 格式,如"https://api.example.com/errors/not-found"
安全的扩展方式:
public class ValidationProblemDetails : ProblemDetails
{
public Dictionary<string, string[]> Errors { get; set; } = new();
// 注意:属性名首字母大写,且不与 RFC 7807 保留字段重名
}
不要在 ProblemDetails 里塞 ex.StackTrace
这是最容易被忽略的安全盲区。堆栈信息包含源码路径、方法名、甚至部分变量值,在生产环境直接返回等于主动泄露攻击面:
- 即使你写了
Detail = ex.Message,也要确认没在其他地方(比如日志输出、自定义中间件、全局过滤器)把StackTrace拼进响应体 - 使用
HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error提取原始异常时,仅用于日志记录,绝不能原样塞进ProblemDetails.Detail - 开发环境可保留简略堆栈(如第一行),生产环境一律清空
Detail或替换为泛化提示:“服务暂时不可用,请稍后重试”
真正需要调试时,靠的是日志 ID + 分布式追踪上下文,不是靠前端传回的堆栈字符串。


















