Api错误码与异常返回规范
错误码体系与异常处理约定
[Requirement] 错误码为 6 位数字,结构为 {模块位 2 位}{类型位 1 位}{序号 3 位},便于定位来源。
40 0 001│ │ └── 序号(001-999)│ └──── 类型:0=通用 1=参数 2=业务 3=权限 4=系统 5=第三方└─────── 模块:10=用户 20=订单 30=支付 40=通用 ...[Standard] 错误码以 int 传输,不用字符串;成功固定为 0。
| 模块 | 范围 | 示例 |
|---|---|---|
| 通用 | 400000-409999 | 400001 参数错误 |
| 认证鉴权 | 410000-419999 | 410001 Token 无效 |
| 权限 | 430000-439999 | 430001 无操作权限 |
| 用户 | 100000-109999 | 100001 用户不存在 |
| 订单 | 200000-209999 | 200001 订单不存在 |
| 支付 | 300000-309999 | 300001 支付失败 |
| 系统 | 500000-509999 | 500001 内部错误 |
| 错误码 | 含义 | HTTP |
|---|---|---|
| 0 | 成功 | 200 |
| 400001 | 参数错误(校验失败) | 400 |
| 400002 | 缺少必填参数 | 400 |
| 404001 | 资源不存在 | 404 |
| 409001 | 资源冲突 | 409 |
| 410001 | 未认证(Token 缺失/失效) | 401 |
| 410002 | Token 已过期 | 401 |
| 430001 | 无权限 | 403 |
| 429001 | 请求过于频繁 | 429 |
| 500001 | 系统内部错误 | 500 |
| 503001 | 服务不可用 | 503 |
[Requirement] 错误码一旦发布不可复用、不可变更含义;废弃时保留占位并标注 deprecated。
[Requirement] 错误响应与成功响应共用信封结构,code 为非 0,data 为 null,并增加 errors 字段承载字段级错误。
{
"code": 400001,
"message": "参数校验失败",
"data": null,
"errors": [
{ "field": "email", "message": "邮箱格式不正确" },
{ "field": "age", "message": "年龄不能小于 0" }
],
"trace_id": "7f3a9c2e1b4d..."
}[Standard] message 为面向用户/调用方的可读提示,不包含堆栈、SQL、内部类名等敏感信息。
[Standard] 字段级错误通过 errors 数组返回,便于前端定位表单字段。
[Standard] HTTP 状态码表达错误类别,code 表达具体业务原因;二者配套使用。
| 错误类别 | HTTP | code 范围 |
|---|---|---|
| 成功 | 200 | 0 |
| 参数错误 | 400 | 400xxx |
| 未认证 | 401 | 410xxx |
| 无权限 | 403 | 430xxx |
| 资源不存在 | 404 | 404xxx |
| 冲突 | 409 | 409xxx |
| 限流 | 429 | 429xxx |
| 服务端错误 | 500 | 500xxx |
| 服务不可用 | 503 | 503xxx |
// 反模式 1:message 泄漏内部信息
{ "code": 500001, "message": "NullPointerException at UserService.java:42" }
// 反模式 2:用 200 传输业务错误
HTTP/1.1 200 OK
{ "code": 400001, "message": "参数错误" } // 应用 HTTP 400
// 反模式 3:无 errors 字段,前端无法定位
{ "code": 400001, "message": "参数错误" }[Standard] 后端异常分为三级,不同级别处理策略不同。
| 级别 | 类型 | 示例 | 处理方式 | 告警 |
|---|---|---|---|---|
| 业务异常 | 可预期、用户可恢复 | 余额不足、参数错误 | 返回业务码,不告警 | 否 |
| 系统异常 | 依赖故障、可重试 | DB 超时、第三方超时 | 重试/降级,记录日志 | 警告 |
| 未知异常 | 代码 Bug、不可预期 | NPE、数组越界 | 记录堆栈,告警 | 紧急 |
[Requirement] 全局异常处理器(@ControllerAdvice / FastAPI exception_handler)统一捕获并转换,禁止在 Controller 层手写 try-catch 包装。
@app.exception_handler(BusinessError)
async def business_error_handler(request, exc: BusinessError):
return JSONResponse(
status_code=exc.http_status,
content={"code": exc.code, "message": exc.message, "data": None, "trace_id": ctx.trace_id},
)[Caution] 禁止在异常消息中拼接用户输入(防止日志注入与信息泄漏),敏感字段需脱敏。
[Requirement] 每个请求必须生成或透传 trace_id(优先从请求头 X-Trace-Id 读取,无则生成 UUID),并写入响应头与响应体。
[Standard] 日志、错误响应、下游调用必须携带同一 trace_id,实现全链路串联。
# 请求
X-Trace-Id: 7f3a9c2e1b4d...
# 响应头
X-Trace-Id: 7f3a9c2e1b4d...[Standard] 系统异常与未知异常必须记录完整堆栈(logger.error(..., exc_info=True)),业务异常仅记录 warning 级别。
[Caution] 日志中禁止打印密码、Token、身份证、手机号原文,需脱敏(手机号 138****1234)。
[Default] 错误码作为 metrics 标签上报,按 code、path、method 维度统计错误率,用于告警与趋势分析。
HTTP/1.1 400 Bad Request
X-Trace-Id: a1b2c3d4
{
"code": 400001,
"message": "参数校验失败",
"data": null,
"errors": [
{ "field": "email", "message": "邮箱格式不正确" }
],
"trace_id": "a1b2c3d4"
}HTTP/1.1 409 Conflict
X-Trace-Id: e5f6g7h8
{
"code": 200003,
"message": "订单已支付,无法重复支付",
"data": null,
"errors": null,
"trace_id": "e5f6g7h8"
}HTTP/1.1 500 Internal Server Error
X-Trace-Id: i9j0k1l2
{
"code": 500001,
"message": "系统内部错误,请稍后重试",
"data": null,
"errors": null,
"trace_id": "i9j0k1l2"
}class AppError(Exception):
code: int = 500001
http_status: int = 500
message: str = "系统内部错误"
def __init__(self, message: str | None = None):
if message:
self.message = message
super().__init__(self.message)
class BusinessError(AppError):
http_status = 400
class ParamError(BusinessError):
code = 400001
message = "参数校验失败"
class NotFoundError(BusinessError):
code = 404001
http_status = 404
message = "资源不存在"
class UnauthorizedError(AppError):
code = 410001
http_status = 401
message = "未认证"