(十七)统一错误响应与失败位置
1. 把错误分成可定位的结果
请求中的错误和服务执行中的失败发生在不同阶段。我们给错误响应统一一层结构:error 下保存 code、message、fields,字段错误再记录位置。业务动作只抛出 Problem,HTTP 层决定怎样返回 JSON。
| 实际请求或操作 | 状态 | 排查起点 |
|---|---|---|
finished 传字符串,或书名全是空格 |
422 |
fields 中的字段位置与类型 |
| 缺少、过期或已退出的令牌 | 401 |
Authorization 与令牌记录 |
| bob 请求修改 alice 的书 | 403 |
已登录用户与书籍归属 |
| 查询没有保存过的书籍编号 | 404 |
路径编号与记录 |
| 同一用户再次保存同名书 | 409 |
唯一键与当前已有数据 |
| 数据库执行失败 | 500 |
服务日志与事务结果 |
# 续写同一文件:api_project/src/bookapi/errors.py
# 把错误代码、说明和字段位置组织成统一模型,路由和异常处理器都复用这一结构。
class FieldIssue(BaseModel):
loc: list[str | int]
type: str
msg: str
class ErrorDetail(BaseModel):
code: str
message: str
fields: list[FieldIssue] = Field(default_factory=list)
class ErrorBody(BaseModel):
error: ErrorDetail先用模型表达错误字段,再把具体异常接到同一结构:
# 续写同一文件:api_project/src/bookapi/errors.py
def install_handlers(app: FastAPI):
def reply(status, code, message, fields=None, headers=None):
return JSONResponse(
status_code=status,
content={
"error": {"code": code, "message": message, "fields": fields or []}
},
headers=headers,
)
@app.exception_handler(Problem)
async def business_error(_request: Request, exc: Problem):
return reply(exc.status, exc.code, exc.message)
@app.exception_handler(RequestValidationError)
async def validation_error(_request: Request, exc: RequestValidationError):
# 不把 input 原值和 ctx 放进响应,密码也不会被错误响应回显。
fields = [
{"loc": list(item["loc"]), "type": item["type"], "msg": item["msg"]}
for item in exc.errors()
]
return reply(422, "validation_error", "请检查请求字段", fields)
@app.exception_handler(HTTPException)
async def http_error(_request: Request, exc: HTTPException):
return reply(
exc.status_code, "http_error", str(exc.detail), headers=exc.headers
)
@app.exception_handler(SQLAlchemyError)
async def database_error(_request: Request, exc: SQLAlchemyError):
# 日志只记录异常类别,不把 SQL 参数中的账号摘要与令牌摘要写进日志。
logger.error("数据库操作失败:%s", type(exc).__name__)
return reply(500, "database_error", "数据库操作失败,请稍后重试")app.exception_handler(...) 登记按异常类型选择的处理器,参数中接收本次 Request 和异常对象。JSONResponse 可以直接设置状态、JSON 内容和头。
RequestValidationError 是 FastAPI 包装后的请求校验错误。exc.errors() 含位置、错误类别、说明,也可能带原输入;这里挑选三项交回,密码不会随着字段错误回显。位置如 ['body', 'finished'] 指请求体中的字段,['query', 'limit'] 指查询参数。
starlette.exceptions.HTTPException 也覆盖框架产生的未找到和方法错误;转成同一结构时保留 exc.headers,让 WWW-Authenticate 和 Allow 仍然进入响应。SQLAlchemyError 统一返回数据库失败,日志记录异常类别,SQL 参数不直接输出。
FieldIssue 描述单个字段问题,ErrorDetail 描述错误内容,ErrorBody 再包上 error。这几个模型供下一节的 responses 参数使用,让接口文档也按相同结构展示错误。处理器直接构造 JSONResponse,因此测试还会核对实际响应与这些字段一致。