7.2 API 错误契约
错误契约决定前端如何展示错误、后端如何记录问题、测试如何断言失败路径。没有统一契约时,前端会被迫解析各种不一致的错误格式。
学习目标
- 能区分校验错误、认证错误、权限错误、资源不存在和服务器错误。
- 能设计统一错误响应结构。
- 能在 ASP.NET Core 中集中处理异常。
- 能让前端按错误类型展示不同提示。
应用场景
- 表单字段校验失败,需要展示字段级提示。
- 用户未登录或 Token 过期。
- 用户访问了没有权限的资源。
- 后端发生未预期异常,需要给用户友好提示并记录日志。
推荐错误结构
{
"code": "todo.title_required",
"message": "标题不能为空",
"traceId": "0HMS...",
"errors": {
"title": ["标题不能为空"]
}
}
状态码约定
| 状态码 | 场景 | 前端处理 |
|---|---|---|
| 400 | 请求格式或业务校验失败 | 展示表单或业务提示 |
| 401 | 未登录或登录过期 | 跳转登录或刷新令牌 |
| 403 | 已登录但无权限 | 展示无权限页面 |
| 404 | 资源不存在 | 展示空状态或不存在提示 |
| 500 | 服务端未知错误 | 展示通用错误并保留 traceId |
ASP.NET Core 示例
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var traceId = context.TraceIdentifier;
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new ApiError(
"server.unexpected_error",
"服务暂时不可用,请稍后再试",
traceId));
});
});
public sealed record ApiError(string Code, string Message, string TraceId);
前端处理示例
export type ApiError = {
code: string;
message: string;
traceId: string;
errors?: Record<string, string[]>;
};
export async function parseApiError(response: Response): Promise<ApiError> {
try {
return (await response.json()) as ApiError;
} catch {
return {
code: 'client.unexpected_response',
message: '服务响应格式异常',
traceId: '',
};
}
}
重点难点
- 错误
code应稳定,message可以面向用户调整。 - 生产环境不要把异常堆栈返回给前端。
traceId是联动前端报错和后端日志的关键。- 校验错误应尽量包含字段级信息,方便表单展示。
常见误区
| 误区 | 推荐做法 |
|---|---|
| 所有失败都返回 200 | 用状态码表达失败类型 |
| 前端靠字符串匹配错误 | 使用稳定 code 判断错误 |
| 直接返回异常详情 | 记录日志,返回友好错误和 traceId |
练习
- 为创建任务接口设计标题为空的错误响应。
- 前端根据 401 自动跳转登录页。
- 在日志中记录错误
code和traceId。