07-错误处理
Python 3.11+
本章讲解 FastAPI 异常处理和自定义错误响应。
第一部分:HTTPException
1.1 实际场景
请求的资源不存在时,需要返回 404 错误和友好的错误信息。
问题:如何返回 HTTP 错误响应?
1.2 基本使用
python
from fastapi import FastAPI, HTTPException
app: FastAPI = FastAPI()
items_db: dict[int, str] = {1: "Item 1"}
@app.get("/items/{item_id}")
def read_item(item_id: int) -> str:
if item_id not in items_db:
raise HTTPException(status_code=404, detail="Item not found")
return items_db[item_id]1.3 自定义状态码
python
from fastapi import status
@app.post("/users")
def create_user(username: str, email: str) -> dict[str, str]:
if email in existing_emails:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
return {"id": "1", "email": email}第二部分:自定义异常
2.1 实际场景
业务逻辑有特定的错误类型,如余额不足、库存不足等,需要统一的错误格式。
问题:如何定义业务异常?
2.2 定义异常类
python
from fastapi import HTTPException
class ItemNotFoundException(HTTPException):
def __init__(self, item_id: int):
super().__init__(
status_code=404,
detail=f"Item {item_id} not found"
)
class ValidationException(HTTPException):
def __init__(self, field: str, message: str):
super().__init__(
status_code=422,
detail={"field": field, "message": message}
)2.3 使用自定义异常
python
@app.get("/items/{item_id}")
def read_item(item_id: int) -> str:
if item_id not in items_db:
raise ItemNotFoundException(item_id)
return items_db[item_id]第三部分:异常处理器
3.1 实际场景
所有错误响应需要统一的 JSON 格式,包含错误码、错误消息等。
问题:如何全局处理异常并格式化响应?
3.2 注册处理器
python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
app: FastAPI = FastAPI()
@app.exception_handler(ItemNotFoundException)
async def item_not_found_handler(request: Request, exc: ItemNotFoundException) -> JSONResponse:
return JSONResponse(
status_code=exc.status_code,
content={"error": "Item Not Found", "message": exc.detail}
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
return JSONResponse(
status_code=422,
content={"error": "Validation Error", "details": exc.errors()}
)3.3 全局错误处理
python
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
return JSONResponse(
status_code=500,
content={"error": "Internal Server Error", "message": str(exc)}
)第四部分:完整示例
python
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from pydantic import BaseModel
app: FastAPI = FastAPI()
# 自定义异常
class BusinessException(Exception):
def __init__(self, code: int, message: str):
self.code: int = code
self.message: str = message
# 异常处理器
@app.exception_handler(BusinessException)
async def business_exception_handler(request: Request, exc: BusinessException) -> JSONResponse:
return JSONResponse(
status_code=exc.code,
content={"error": exc.message}
)
@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
return JSONResponse(
status_code=422,
content={
"error": "Validation Error",
"details": exc.errors()
}
)
# 模型
class Item(BaseModel):
name: str
price: float
# 路由
items_db: dict[int, dict[str, str | float]] = {1: {"name": "Apple", "price": 1.5}}
@app.get("/items/{item_id}")
def get_item(item_id: int) -> dict[str, str | float]:
if item_id not in items_db:
raise HTTPException(status_code=404, detail="Item not found")
return items_db[item_id]
@app.post("/items")
def create_item(item: Item) -> dict[str, int | dict]:
if item.price < 0:
raise BusinessException(400, "Price cannot be negative")
item_id: int = len(items_db) + 1
items_db[item_id] = item.model_dump()
return {"id": item_id, "item": item.model_dump()}
@app.get("/error")
def trigger_error():
raise BusinessException(418, "I'm a teapot")第五部分:L3 专家层
5.1 Starlette 的异常处理链
FastAPI 的异常处理基于 Starlette 的 ExceptionHandlerMiddleware,采用从具体到通用的匹配策略:
异常抛出
│
▼
┌──────────────────────────────────┐
│ ExceptionHandlerMiddleware │
│ ┌────────────────────────────┐ │
│ │ 1. 精确匹配 │ │
│ │ ItemNotFoundException │ │────→ 注册了处理器?是 → 执行 → 返回响应
│ └────────────────────────────┘ │
│ ┌────────────────────────────┐ │
│ │ 2. 继承链向上查找 │ │
│ │ HTTPException ──→ ValueError │────→ 注册了处理器?是 → 执行 → 返回响应
│ └────────────────────────────┘ │
│ ┌────────────────────────────┐ │
│ │ 3. 422 Validation Error │ │────→ Pydantic 校验失败时触发
│ └────────────────────────────┘ │
│ ┌────────────────────────────┐ │
│ │ 4. 全局 Exception 处理器 │ │────→ 最后的兜底
│ └────────────────────────────┘ │
│ ┌────────────────────────────┐ │
│ │ 5. 默认行为 │ │────→ 500 Internal Server Error
│ └────────────────────────────┘ │
└──────────────────────────────────┘python
# starlette/middleware/exceptions.py 核心逻辑(简化版)
class ExceptionHandlerMiddleware:
async def __call__(self, scope, receive, send):
try:
await self.app(scope, receive, send)
except Exception as exc:
handler = self._lookup_handler(exc)
if handler:
response = await handler(request, exc)
await response(scope, receive, send)
else:
raise exc # 继续向上抛
def _lookup_handler(self, exc: Exception) -> Callable | None:
# 先查精确匹配
if type(exc) in self.exception_handlers:
return self.exception_handlers[type(exc)]
# 再查 MRO(方法解析顺序)中的父类
for cls in type(exc).__mro__:
if cls in self.exception_handlers:
return self.exception_handlers[cls]
return None5.2 HTTPException vs ValidationError
| 对比维度 | HTTPException | RequestValidationError |
|---|---|---|
| 触发时机 | 开发者主动 raise | Pydantic 参数校验失败自动抛出 |
| 状态码 | 开发者自定义 | 固定 422 Unprocessable Entity |
| 响应格式 | {"detail": "..."} | {"detail": [{"loc": [...], "msg": "...", "type": "..."}]} |
| 异常类型 | fastapi.HTTPException | fastapi.exceptions.RequestValidationError |
| 处理器注册 | @app.exception_handler(HTTPException) | @app.exception_handler(RequestValidationError) |
| 拦截点 | 路由函数内部 | 路由函数执行前(FastAPI 依赖注入阶段) |
python
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
# 自定义 422 响应格式(简化 detail 结构)
@app.exception_handler(RequestValidationError)
async def custom_validation_handler(
request: Request,
exc: RequestValidationError,
) -> JSONResponse:
simplified_errors: list[dict[str, str]] = []
for error in exc.errors():
simplified_errors.append({
"field": ".".join(str(loc) for loc in error["loc"]),
"message": error["msg"],
})
return JSONResponse(
status_code=422,
content={"error": "Validation Failed", "errors": simplified_errors},
)5.3 全局异常处理器的注册机制
FastAPI 提供三层异常处理器注册:
python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from starlette.responses import PlainTextResponse
app: FastAPI = FastAPI()
# 层级 1:自定义异常 —— 精确匹配
@app.exception_handler(BusinessException)
async def handle_business(request: Request, exc: BusinessException) -> JSONResponse:
return JSONResponse(status_code=exc.code, content={"error": exc.message})
# 层级 2:框架异常 —— 覆盖默认行为
@app.exception_handler(RequestValidationError)
async def handle_validation(request: Request, exc: RequestValidationError) -> JSONResponse:
return JSONResponse(status_code=422, content={"error": "Invalid input"})
# 层级 3:全局兜底 —— 捕获所有未处理异常
@app.exception_handler(Exception)
async def handle_global(request: Request, exc: Exception) -> JSONResponse:
# 生产环境应记录日志而非暴露异常细节
import logging
logging.error(f"Unhandled exception: {exc}", exc_info=True)
return JSONResponse(status_code=500, content={"error": "Internal Server Error"})注册机制内部流程:
@app.exception_handler(SomeException)
│
▼
app.exception_handlers[SomeException] = handler_fn
│
▼
Starlette 创建 ExceptionHandlerMiddleware
│
├── exception_handlers: dict[type, Callable]
└── 使用场景:__call__ 捕获异常后查表| 注册方式 | 作用域 | 优先级 |
|---|---|---|
@app.exception_handler(CustomException) | 应用级 | 高(精确匹配) |
@app.exception_handler(HTTPException) | 应用级 | 中(覆盖默认) |
@app.exception_handler(Exception) | 应用级 | 低(兜底) |
APIRouter(exception_handler=...) | 路由级 | 最高(局部覆盖) |
5.4 性能考量
| 操作 | 耗时级别 | 说明 |
|---|---|---|
HTTPException 抛出 | ~0.001ms | 纯 Python 异常,无额外开销 |
| 异常处理器查表 | ~0.001ms | dict 精确匹配 + MRO 线性查找 |
RequestValidationError | ~0.01ms | Pydantic 已做校验,构造 error list 开销小 |
| 全局 Exception 处理器 | ~0.001ms | 最后兜底,仅记录日志 |
| 异常栈生成 | ~0.1ms | exc_info=True 时生成 traceback |
5.5 设计动机
| 设计选择 | 动机 |
|---|---|
HTTPException 而非 HTTP 状态码直接返回 | 统一异常抛出方式,集中由中间件处理响应格式 |
| 校验失败返回 422 而非 400 | 422 语义更准确(请求格式正确但语义校验失败) |
| 异常处理器注册机制 | 解耦业务逻辑与错误响应格式,便于全局统一管理 |
| MRO 继承链查找 | 子类的异常若未注册处理器,自动回退到父类处理器 |
全局 Exception 兜底 | 防止未捕获异常导致 500 默认 HTML 页面暴露内部信息 |
5.6 知识关联
错误处理
├── HTTPException
│ ├── status_code: HTTP 状态码
│ ├── detail: 错误详情(string / dict / list)
│ └── headers: 额外响应头(如 WWW-Authenticate)
│
├── 异常处理链(Starlette)
│ ├── 精确匹配 type(exc)
│ ├── MRO 继承链查找父类
│ └── 未匹配则向上抛出
│
├── 校验错误(422)
│ ├── RequestValidationError:请求体/路径/查询参数校验失败
│ ├── WebSocketRequestValidationError:WebSocket 校验失败
│ └── 错误结构:[{"loc": ["body", "name"], "msg": "...", "type": "..."}]
│
├── 异常处理器注册
│ ├── @app.exception_handler(SomeException)
│ ├── APIRouter 局部处理器
│ └── 全局 Exception 兜底
│
└── 自定义异常模式
├── 继承 HTTPException(简单)
├── 继承 Exception + 独立处理器(业务解耦)
└── BusinessException 统一错误码体系总结
| 知识点 | 说明 |
|---|---|
| HTTPException | HTTP 异常 |
| 自定义异常 | 业务异常 |
| 异常处理器 | 全局处理 |
| 错误响应 | 统一格式 |