Skip to content

第 9 章 · 错误处理与自定义异常

本章目标:掌握 HTTPException 的正确用法,学会注册自定义异常处理器、覆盖默认校验错误响应,并能设计一套统一的错误响应结构。

9.1 用 raise 而不是 return

客户端出错时应返回 400~499 区间的状态码。FastAPI 的惯用法是抛出 HTTPException

python
from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        # 注意:是 raise,不是 return
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

为什么用 raise?因为 HTTPException 就是一个普通 Python 异常——它可以从深层工具函数里一路冒泡出来:只要任何一层代码 raise 了它,当前请求立即终止并把错误响应发给客户端,路径函数剩余代码不再执行。如果用 return,你必须在每一层手动传递和判断。

detail 参数不限于字符串,传 dictlist 等任何可 JSON 化的值都可以,最终会出现在响应体的 "detail" 字段中。

9.2 附加响应头与自定义异常处理器

某些场景需要给错误响应加自定义头(典型的如认证失败时返回 WWW-Authenticate):

python
raise HTTPException(
    status_code=401,
    detail="登录已过期",
    headers={"WWW-Authenticate": "Bearer"},
)

对于业务领域里反复出现的错误类型,更优雅的做法是定义自己的异常类并注册全局处理器:

python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse


class PaymentError(Exception):
    """业务自定义异常:支付失败"""

    def __init__(self, reason: str, order_id: str):
        self.reason = reason
        self.order_id = order_id


app = FastAPI()


@app.exception_handler(PaymentError)          # 注册:遇到该异常就交给这个函数
async def payment_error_handler(
    request: Request, exc: PaymentError
):
    return JSONResponse(
        status_code=402,                      # Payment Required,按需选择状态码
        content={
            "code": "PAYMENT_FAILED",
            "message": exc.reason,
            "order_id": exc.order_id,
        },
    )


@app.post("/pay/{order_id}")
async def pay(order_id: str):
    raise PaymentError("余额不足", order_id)   # 业务代码只管抛,处理器统一兜底

这样业务代码保持干净:不需要在每个接口写 try/except,异常处理策略集中在一处。

::: note 技术细节 这些异常工具直接来自 Starlette;fastapi.responses.JSONResponse 只是 Starlette 同名类的便捷别名。 :::

9.3 覆盖默认异常处理器

FastAPI 内置了两个默认处理器:处理 HTTPException 和处理请求数据校验失败的 RequestValidationError。两者都可以覆盖。

自定义校验错误的响应格式

前端团队常常嫌默认的 422 响应太啰嗦(嵌套的 loc/msg/type 数组),可以整体替换:

python
from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

app = FastAPI()


@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
    # 把 pydantic 错误列表压平成前端友好的 {字段: 错误消息} 映射
    errors = {}
    for err in exc.errors():
        field = ".".join(str(p) for p in err["loc"][1:]) or "body"
        errors.setdefault(field, err["msg"])
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content=jsonable_encoder({"code": "INVALID_INPUT", "errors": errors}),
    )

RequestValidationError 还带有 .body 属性(导致失败的原始请求体),开发环境可以把它加进日志方便排查。

覆盖 HTTPException 处理器时的一个关键细节

FastAPI 自己的 HTTPException 继承自 Starlette 的 HTTPException,唯一区别是 FastAPI 版允许 detail 为任意 JSON 数据。因此官方明确建议:

注册处理器时要针对 Starlette 的 HTTPException,这样 Starlette 内部或第三方插件抛出的 HTTPException 也能被你的处理器捕获。

python
from starlette.exceptions import HTTPException as StarletteHTTPException


@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc: StarletteHTTPException):
    return JSONResponse(status_code=exc.status_code,
                        content={"code": exc.status_code, "message": exc.detail},
                        headers=getattr(exc, "headers", None))

9.4 设计统一的错误响应结构

生产 API 推荐所有错误长一个样,前端只需要写一次解析逻辑。常见约定:

json
{
  "code": "ORDER_NOT_FOUND",
  "message": "订单 10086 不存在",
  "request_id": "req-8f3a"
}

落地思路分三层:

  1. 可预期的业务错误 → 定义异常类 + @app.exception_handler 统一转格式;
  2. 参数校验错误 → 覆盖 RequestValidationError 处理器;
  3. 未预期异常 → 兜底注册 Exception 处理器记录日志并返回 500,同时注意:不要把堆栈信息泄露给客户端(exc.errors() 中可能含内部实现细节,对外输出前要清洗)。

还可以复用 FastAPI 默认处理器做"增强"而非完全替代:

python
from fastapi.exception_handlers import (
    http_exception_handler,
    request_validation_exception_handler,
)


@app.exception_handler(RequestValidationError)
async def log_then_default(request: Request, exc: RequestValidationError):
    print(f"[校验失败] body={exc.body!r}")   # 先记日志
    return await request_validation_exception_handler(request, exc)  # 再走默认行为

9.5 本章小结

  • HTTPException 要用 raise 触发,可从任意深度的工具函数冒泡,detail 支持任意 JSON 数据;
  • 自定义异常 + @app.exception_handler(ExcClass) 是组织业务错误的标准姿势;
  • 校验错误走 RequestValidationError 处理器;其 .body 可用于调试;
  • HTTPException 注册处理器时要用 Starlette 的 HTTPException 类以扩大捕获范围;
  • 统一错误结构 = 异常类分层(业务/校验/未知),未预期异常必须记日志且不泄露内部细节。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 在工具函数中 raise HTTPException 后,调用它的路径操作函数剩余代码会怎样?

2. 为 HTTPException 注册自定义处理器时,应该针对哪个类注册?

3. 当请求参数校验失败时,FastAPI 内部抛出的异常和处理方式是?

4. 关于统一错误响应设计,下列做法最不可取的是?

🛠️ 动手实践

  1. 定义 RateLimitError 异常并在处理器中返回 429 与 Retry-After 响应头,把它接入第 8 章实践 3 的限流逻辑。
  2. 覆盖 RequestValidationError 处理器:把错误压平为 {字段名: 消息} 映射,同时在开发模式下把 exc.body 写入日志。
  3. 实现一个兜底的 Exception 处理器:记录带时间戳的错误日志、返回不含任何内部信息的 {"code": "INTERNAL_ERROR"},并用一个故意 /0 的路由验证它生效。

错误处理体系搭建完毕。下一章是 FastAPI 最强大的武器:第 10 章 · 依赖注入系统基础