#异常处理(Exception Handling):全局异常捕获与自定义错误响应
📂 所属阶段:第二阶段 — 进阶黑科技(核心篇)
🔗 相关章节:中间件应用 · APIRouter 模块化
#1. FastAPI 异常处理机制
#1.1 FastAPI 的异常体系
HTTPException(业务异常)
├── 400 Bad Request
├── 401 Unauthorized
├── 403 Forbidden
├── 404 Not Found
├── 422 Validation Error
└── 自定义 5xx
RequestValidationError(请求验证异常)
StarletteHTTPException(基类)
Python 内置异常(ValueError, KeyError, etc.)#1.2 抛出 HTTPException
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"1": "Apple", "2": "Banana", "3": "Orange"}
@app.get("/items/{item_id}")
async def get_item(item_id: str):
if item_id not in items:
# 抛出 HTTPException,FastAPI 自动转为 JSON 响应
raise HTTPException(
status_code=404,
detail=f"Item {item_id} not found", # 自定义错误信息
headers={"X-Error": "ItemMissing"}
)
return {"item_id": item_id, "name": items[item_id]}FastAPI 会自动将其转为:
{
"detail": "Item 99 not found"
}#2. 全局异常处理器
#2.1 为什么需要全局处理?
- 路由中未捕获的异常会导致 500 Internal Server Error
- 不同路由的异常格式不统一
- 线上暴露异常栈信息有安全风险
#2.2 注册全局异常处理器
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
import traceback
import logging
logger = logging.getLogger(__name__)
app = FastAPI()
# ── 1. HTTPException 处理器 ────────────────────────
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
"code": exc.status_code,
"message": exc.detail,
"path": str(request.url),
}
)
# ── 2. 请求验证异常处理器(参数校验失败)─────────────
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = []
for error in exc.errors():
field = ".".join(str(loc) for loc in error["loc"])
errors.append({"field": field, "message": error["msg"]})
return JSONResponse(
status_code=422,
content={
"code": 422,
"message": "参数校验失败",
"errors": errors,
}
)
# ── 3. 通用异常处理器(兜底)───────────────────────
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
# 生产环境:记录日志,返回通用错误
logger.error(f"Unhandled error: {exc}\n{traceback.format_exc()}")
return JSONResponse(
status_code=500,
content={
"code": 500,
"message": "服务器内部错误,请稍后重试",
# 开发环境可以返回更多信息:
# "detail": str(exc),
# "traceback": traceback.format_exc(),
}
)#2.3 自定义业务异常类
# exceptions.py — 统一的自定义异常
class AppException(Exception):
"""基础业务异常"""
def __init__(self, message: str, code: int = 400, details: dict = None):
self.message = message
self.code = code
self.details = details or {}
super().__init__(message)
class NotFoundException(AppException):
"""资源不存在"""
def __init__(self, resource: str, identifier: str):
super().__init__(
message=f"{resource} '{identifier}' 不存在",
code=404,
details={"resource": resource, "identifier": identifier}
)
class UnauthorizedException(AppException):
"""未授权"""
def __init__(self, reason: str = "请先登录"):
super().__init__(message=reason, code=401)
class ForbiddenException(AppException):
"""权限不足"""
def __init__(self):
super().__init__(message="权限不足,无法访问该资源", code=403)
class RateLimitException(AppException):
"""请求过于频繁"""
def __init__(self, retry_after: int = 60):
super().__init__(
message="请求过于频繁,请稍后重试",
code=429,
details={"retry_after": retry_after}
)# 注册业务异常处理器
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
content = {
"code": exc.code,
"message": exc.message,
}
if exc.details:
content["details"] = exc.details
response = JSONResponse(status_code=exc.code, content=content)
if isinstance(exc, RateLimitException):
response.headers["Retry-After"] = str(exc.details.get("retry_after", 60))
return response#2.4 使用自定义异常
from exceptions import NotFoundException, UnauthorizedException
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = await db.find_user(user_id)
if not user:
raise NotFoundException("用户", str(user_id))
return user
@app.delete("/articles/{article_id}")
async def delete_article(article_id: int, token: str = Header(...)):
user = await auth.verify(token)
article = await db.find_article(article_id)
if article.author_id != user.id:
raise UnauthorizedException("无权删除他人文章")
await db.delete(article)
return {"deleted": article_id}#3. 统一响应格式
#3.1 响应模型封装
from pydantic import BaseModel
from typing import TypeVar, Generic, Optional, Any
T = TypeVar("T")
class ApiResponse(BaseModel, Generic[T]):
"""统一响应格式"""
code: int = 200
message: str = "success"
data: Optional[T] = None
error: Optional[str] = None
class Config:
json_schema_extra = {
"example": {
"code": 200,
"message": "success",
"data": {"id": 1, "name": "Alice"}
}
}
def success_response(data: Any = None, message: str = "success"):
return ApiResponse(code=200, message=message, data=data)
def error_response(code: int, message: str, error: str = None):
return ApiResponse(code=code, message=message, error=error)#3.2 在路由中使用
from fastapi import APIRouter
from typing import List
router = APIRouter()
@router.get("/users", response_model=ApiResponse[List[dict]])
async def list_users():
users = await db.query_users()
return success_response(data=users, message="获取用户列表成功")
@router.get("/users/{user_id}", response_model=ApiResponse[dict])
async def get_user(user_id: int):
user = await db.find_user(user_id)
if not user:
return error_response(404, "用户不存在")
return success_response(data=user)#4. 异常处理与日志
#4.1 结构化日志
import structlog
import logging
# 配置结构化日志
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.stdlib.BoundLogger,
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
logger = structlog.get_logger()
@app.exception_handler(Exception)
async def log_exception(request: Request, exc: Exception):
logger.error(
"request_failed",
method=request.method,
path=str(request.url.path),
error_type=type(exc).__name__,
error_message=str(exc),
traceback=traceback.format_exc(),
)
return JSONResponse(
status_code=500,
content={"code": 500, "message": "Internal server error"}
)#4.2 按异常类型差异化处理
@app.exception_handler(Exception)
async def smart_exception_handler(request: Request, exc: Exception):
if isinstance(exc, AppException):
# 业务异常:直接返回业务定义的错误
return JSONResponse(
status_code=exc.code,
content={"code": exc.code, "message": exc.message}
)
if isinstance(exc, ValueError):
# 业务参数错误 → 400
return JSONResponse(400, {"code": 400, "message": str(exc)})
if isinstance(exc, ConnectionError):
# 服务不可用 → 503
logger.error("service_unavailable", error=str(exc))
return JSONResponse(503, {"code": 503, "message": "服务暂时不可用"})
# 未知异常 → 500 + 记录
logger.exception("unhandled_error")
return JSONResponse(500, {"code": 500, "message": "服务器错误"})#5. 404 处理
#5.1 全局 404 处理器
from starlette.requests import Request
from starlette.responses import JSONResponse
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
if exc.status_code == 404:
return JSONResponse(
status_code=404,
content={
"code": 404,
"message": f"API 路径 '{request.url.path}' 不存在",
"suggestion": "请检查 URL 是否正确,或访问 /docs 查看可用接口"
}
)
# 其他 HTTP 异常走通用处理
return JSONResponse(
status_code=exc.status_code,
content={"code": exc.status_code, "message": exc.detail}
)#6. 验证错误详解
#6.1 RequestValidationError 结构
# 触发 422 验证错误
@app.get("/calculate")
async def calculate(x: int, y: int):
return {"result": x / y}
# GET /calculate?x=10&y=0 → 除零错误(Python 异常,非验证异常)
# GET /calculate?x=abc&y=2 → 参数类型验证错误(RequestValidationError)验证错误响应示例:
{
"code": 422,
"message": "参数校验失败",
"errors": [
{
"field": "body.price",
"message": "Input should be greater than or equal to 0"
}
]
}#6.2 自定义验证错误消息
from pydantic import BaseModel, field_validator
class CreateUser(BaseModel):
name: str
email: str
age: int
@field_validator("name")
@classmethod
def name_must_not_be_empty(cls, v: str) -> str:
if not v.strip():
raise ValueError("用户名不能为空")
return v.strip()
@field_validator("email")
@classmethod
def email_format(cls, v: str) -> str:
if "@" not in v:
raise ValueError("邮箱格式不正确")
return v.lower()
@field_validator("age")
@classmethod
def age_range(cls, v: int) -> int:
if v < 0 or v > 150:
raise ValueError("年龄必须在 0-150 之间")
return v
@app.post("/users")
async def create_user(user: CreateUser):
return {"created": user.model_dump()}#7. 异常处理最佳实践
✅ 推荐:
1. 自定义业务异常类,统一错误码和消息格式
2. 异常处理器返回统一的 JSON 结构
3. 生产环境不要暴露异常堆栈
4. 记录详细日志供排查
5. HTTPException 用于"预期内的错误"
6. 全局兜底异常处理器防止意外 500 泄漏
❌ 避免:
1. 路由中用 try/except 吞掉所有异常但不处理
2. 在 except 中 print 而不记录日志
3. 混用不同的错误响应格式
4. 过于宽泛的 except Exception#8. 小结
| 异常类型 | 触发场景 | FastAPI 默认响应 |
|---|---|---|
HTTPException | 业务逻辑(资源不存在、权限不足) | {"detail": "..."} |
RequestValidationError | 参数校验失败 | 详细错误位置列表 |
Exception | 所有未捕获异常 | 500(需自定义兜底) |
自定义 AppException | 业务特定错误 | 自定义结构 |
💡 核心原则:异常处理的目标是让客户端收到可预期的错误响应,同时让服务端记录足够排查问题的日志。
🔗 扩展阅读

