Python 现代开发学习笔记

(十七)统一错误响应与失败位置

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 参数不直接输出。

同一次失败沿参数校验身份归属或数据库执行停在对应位置,形成统一 JSON,并在数据库异常路径回滚后由新请求核对记录

FieldIssue 描述单个字段问题,ErrorDetail 描述错误内容,ErrorBody 再包上 error。这几个模型供下一节的 responses 参数使用,让接口文档也按相同结构展示错误。处理器直接构造 JSONResponse,因此测试还会核对实际响应与这些字段一致。

笔记正文与全部题解 · Ctrl / ⌘ K