63. FastAPI 捕获异常
一、异常使用的场景和基础使用
什么时候需要异常
异常不是"程序出 bug"的专属,它也是接口表达业务结果的一种方式。典型场景:
- 参数不合法(如
page小于 1)→ 返回 400。 - 资源不存在(如商品、用户查不到)→ 返回 404。
- 没有权限(未登录、角色不符)→ 返回 401 / 403。
- 业务规则冲突(库存不足、用户名重复)→ 自定义业务异常。
- 服务器内部错误(数据库断连、逻辑 bug)→ 兜底 500。
前四类是"能预期、要给客户端明确信息"的错误,最后一类是"没料到、不应把细节暴露给客户端"的错误。区分这两类,是设计异常处理的第一步。
HTTPException 基础使用
HTTPException 是 FastAPI 里最常用的异常,主动 raise 即可,框架会把它的 status_code 和 detail 转成 JSON 响应。
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id <= 0:
raise HTTPException(status_code=400, detail="item_id 必须大于 0")
if item_id == 999:
raise HTTPException(
status_code=404,
detail="商品不存在",
headers={"X-Error": "not-found"},
)
return {"item_id": item_id}请求 /items/0 返回 400 与 {"detail": "item_id 必须大于 0"}。headers 可以顺带加响应头。
raise HTTPException 之后的路由代码不会再执行,异常会沿框架默认处理器转成响应。默认情况下它返回 {"detail": ...},要换成自定义结构(如 {code, message}),就需要自己覆盖处理器——这正是下一节的内容。
二、异常处理器绑定到全局、模块、单个路由
异常处理器的查找顺序是"由内向外":路由内就地处理优先,其次是所属模块(子应用)的处理器,最后才是全局处理器。
全局级:app.exception_handler
全局级用 app.exception_handler 装饰器(等价于 app.add_exception_handler)注册,对整个应用生效。它最适合自定义业务异常,把它们转成统一的错误响应。
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
class ItemNotFoundError(Exception):
def __init__(self, item_id: int):
self.item_id = item_id
@app.exception_handler(ItemNotFoundError)
async def item_not_found_handler(request: Request, exc: ItemNotFoundError):
return JSONResponse(
status_code=404,
content={"code": 404, "message": f"商品 {exc.item_id} 不存在"},
)
@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id != 1:
raise ItemNotFoundError(item_id)
return {"item_id": item_id}处理器函数的参数是 request 和 exc;exc 就是抛出的异常对象,可以从中读取自定义字段(这里是 item_id)。
模块级:子应用 exception_handler
和中间件、CORS 类似,FastAPI 的异常处理器也只能挂在 FastAPI 应用上,不能直接挂在 APIRouter。要"模块级",就给每个模块一个子应用、各自注册处理器,再 mount 到主应用。
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
class ModuleError(Exception):
def __init__(self, message: str):
self.message = message
users_app = FastAPI()
@users_app.exception_handler(ModuleError)
async def module_error_handler(request: Request, exc: ModuleError):
return JSONResponse(status_code=400, content={"code": 400, "message": exc.message})
@users_app.get("/users/{user_id}")
def detail(user_id: int):
if user_id <= 0:
raise ModuleError("用户 ID 必须大于 0")
return {"id": user_id}
app = FastAPI()
app.mount("/api/users", users_app)users_app 里的处理器只对用户模块生效。异常先经过子应用的处理器,子应用没处理才继续向上抛给主应用——这就是"由内向外"的含义。
路由级:try / except
FastAPI 没有原生的"单个路由异常处理器"。路由级通常用 try / except 就地处理,把业务异常转成 HTTPException,或直接返回自定义响应。
from fastapi import FastAPI, HTTPException
app = FastAPI()
def find_item(item_id: int):
if item_id != 1:
raise ValueError("商品不存在")
return {"id": item_id, "name": "键盘"}
@app.get("/items/{item_id}")
def read_item(item_id: int):
try:
return find_item(item_id)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc))适合"只有这一个接口"需要特殊处理的情况;如果多个接口都有同样的转换逻辑,就该把它上移到模块级或全局级处理器,避免到处重复 try / except。
返回自定义数据与异常信息
统一返回结构(如 {code, message, data})最常见的做法,是覆盖 FastAPI 内置的 HTTPException 处理器:
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={"code": exc.status_code, "message": exc.detail},
headers=exc.headers,
)
@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id <= 0:
raise HTTPException(status_code=400, detail="item_id 必须大于 0")
return {"item_id": item_id}exc.status_code、exc.detail、exc.headers 都是 HTTPException 自带的字段,可以直接取出来拼进自定义结构。
校验失败的错误同样可以覆盖,并取回更详细的异常信息:
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={
"code": 422,
"message": "参数校验失败",
"errors": exc.errors(),
},
)
@app.get("/items")
def items(page: int, size: int):
return {"page": page, "size": size}请求 /items?page=abc 时,page 无法转成 int,框架抛出 RequestValidationError,被这里的处理器接住,exc.errors() 返回每条字段的具体错误(loc、msg、type 等),exc.body 则能拿到请求体。
三、四类异常的区别
对比总览
| 异常 | 由谁抛出 | 默认状态码 | 默认响应 | 是否要自己注册处理器 |
|---|---|---|---|---|
HTTPException | 你主动 raise | 手动指定 | {"detail": ...} | 否(内置),可覆盖统一格式 |
RequestValidationError | 框架自动(Pydantic) | 422 | 错误详情列表 | 否(内置),可覆盖 |
| 自定义异常 | 你的业务代码 | 无 | 未处理会变 500 | 是,需要注册 handler |
通用 Exception | 任何未处理错误 | 500 | Internal Server Error | 可选(作为兜底) |
HTTPException
表示"客户端能理解的 HTTP 错误",主动抛出,携带 status_code、detail、headers。它由 FastAPI 内置处理器转成 {"detail": ...};覆盖该处理器即可统一成你自己的格式。它是业务错误的主要出口,不属于"程序崩溃"。
RequestValidationError(pydantic 验证失败)
当请求参数、请求体不符合类型声明或 Pydantic 模型时,框架自动抛出,默认 422。它继承自 HTTPException,所以 exc.errors() 能拿到逐字段错误,exc.body 能拿到原始请求体。适合在覆盖后返回更友好的校验提示。
注意:这里说的是请求校验。response_model 在响应阶段校验失败会抛另一个异常 ResponseValidationError,通常意味着服务端返回结构有 bug,会表现为 500。
自定义异常
继承 Exception,用来表达业务语义(如 库存不足、用户不存在、无权操作)。它不会自动变成合适的 HTTP 响应——不注册处理器时,未捕获的自定义异常会一路向上变成 500。所以自定义异常要么配合 app.exception_handler 注册,要么在路由内 try / except 后转成 HTTPException。
通用 Exception 兜底
注册 Exception 处理器可以兜住所有没被专门处理的异常,返回统一的 500:
import logging
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
logger = logging.getLogger("app")
app = FastAPI()
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
# 生产上应记录完整堆栈,便于定位问题
logger.error("未处理异常: %r", exc)
return JSONResponse(
status_code=500,
content={"code": 500, "message": "服务器内部错误"},
)要点:不要把内部错误细节(堆栈、SQL、文件路径)返回给客户端,否则会泄露信息;记录日志交给服务端。这个兜底处理器适合做"最后一道防线",但不应掩盖真正需要修复的 bug。
继承关系与匹配优先级
Starlette 按异常的 MRO(继承链)从最具体到最宽泛查找处理器。因为 RequestValidationError 继承自 HTTPException:
- 同时注册了
HTTPException和RequestValidationError处理器时,验证错误会命中更具体的RequestValidationError处理器。 - 只注册了
HTTPException处理器时,验证错误会落进HTTPException处理器。
所以覆盖 HTTPException 做统一格式时,要留意验证错误是否也被它接住;需要单独处理验证错误,就再注册一个更具体的 RequestValidationError 处理器。
练习:注册一个 库存不足 自定义异常并挂到全局处理器;覆盖 HTTPException 统一返回 {code, message},再覆盖 RequestValidationError 返回逐字段错误;最后用一个子应用演示模块级处理器,验证"由内向外"的查找顺序。