第 9 章 · 错误处理与自定义异常
本章目标:掌握
HTTPException的正确用法,学会注册自定义异常处理器、覆盖默认校验错误响应,并能设计一套统一的错误响应结构。
9.1 用 raise 而不是 return
客户端出错时应返回 400~499 区间的状态码。FastAPI 的惯用法是抛出 HTTPException:
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 参数不限于字符串,传 dict、list 等任何可 JSON 化的值都可以,最终会出现在响应体的 "detail" 字段中。
9.2 附加响应头与自定义异常处理器
某些场景需要给错误响应加自定义头(典型的如认证失败时返回 WWW-Authenticate):
raise HTTPException(
status_code=401,
detail="登录已过期",
headers={"WWW-Authenticate": "Bearer"},
)对于业务领域里反复出现的错误类型,更优雅的做法是定义自己的异常类并注册全局处理器:
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 数组),可以整体替换:
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也能被你的处理器捕获。
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 推荐所有错误长一个样,前端只需要写一次解析逻辑。常见约定:
{
"code": "ORDER_NOT_FOUND",
"message": "订单 10086 不存在",
"request_id": "req-8f3a"
}落地思路分三层:
- 可预期的业务错误 → 定义异常类 +
@app.exception_handler统一转格式; - 参数校验错误 → 覆盖
RequestValidationError处理器; - 未预期异常 → 兜底注册
Exception处理器记录日志并返回 500,同时注意:不要把堆栈信息泄露给客户端(exc.errors()中可能含内部实现细节,对外输出前要清洗)。
还可以复用 FastAPI 默认处理器做"增强"而非完全替代:
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. 关于统一错误响应设计,下列做法最不可取的是?
🛠️ 动手实践
- 定义
RateLimitError异常并在处理器中返回 429 与Retry-After响应头,把它接入第 8 章实践 3 的限流逻辑。 - 覆盖
RequestValidationError处理器:把错误压平为{字段名: 消息}映射,同时在开发模式下把exc.body写入日志。 - 实现一个兜底的
Exception处理器:记录带时间戳的错误日志、返回不含任何内部信息的{"code": "INTERNAL_ERROR"},并用一个故意/0的路由验证它生效。
错误处理体系搭建完毕。下一章是 FastAPI 最强大的武器:第 10 章 · 依赖注入系统基础。