FastAPI异常处理完全指南

📂 所属阶段:第二阶段 — 进阶黑科技(核心篇)
🔗 相关章节:FastAPI中间件应用 · FastAPI APIRouter模块化


一、FastAPI异常处理基础

1.1 异常体系结构

FastAPI 直接继承 Starlette 的异常系统,覆盖「业务预期错误」「请求验证错误」「兜底未知错误」三类核心场景:

StarletteHTTPException(基类)
    ├── HTTPException(业务可控异常)
    │   ├── 400/401/403/404/422/自定义5xx
    │   └── 可附加 headers/自定义 detail
    └── RequestValidationError(Pydantic/请求自动验证错误)

Python 内置异常(ValueError/KeyError/...)
    └── 未路由捕获时 → 500 Internal Server Error(需兜底)

1.2 基础的 HTTPException 使用

直接抛出 HTTPException 即可触发 FastAPI 自动响应:

from fastapi import FastAPI, HTTPException

app = FastAPI()
fruits = {"1": "Apple", "2": "Banana", "3": "Orange"}

@app.get("/fruits/{fruit_id}")
async def get_fruit(fruit_id: str):
    if fruit_id not in fruits:
        raise HTTPException(
            status_code=404,
            detail=f"Fruit {fruit_id} not found",
            headers={"X-Error-Type": "FruitMissing"}  # 可选附加header
        )
    return {"fruit_id": fruit_id, "name": fruits[fruit_id]}

默认响应为简洁的 JSON:

{"detail": "Fruit 99 not found"}

二、全局异常处理器

单路由的 try/except 会导致格式混乱、敏感信息泄露,全局处理器是企业级开发的首选

2.1 核心处理器体系

注册以下三类处理器覆盖99%场景:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
import logging
import traceback
from datetime import datetime

logger = logging.getLogger(__name__)
app = FastAPI()

# 1. StarletteHTTPException(含FastAPI的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.path),
            "timestamp": datetime.utcnow().isoformat()
        }
    )

# 2. 请求验证异常(Pydantic自动校验失败)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    errors = []
    for err in exc.errors():
        field = ".".join(str(loc) for loc in err["loc"])  # 格式化错误字段路径
        errors.append({"field": field, "msg": err["msg"]})
    return JSONResponse(
        status_code=422,
        content={
            "code": 422,
            "message": "请求参数校验失败",
            "errors": errors,
            "path": str(request.url.path),
            "timestamp": datetime.utcnow().isoformat()
        }
    )

# 3. 兜底通用异常(未路由/未预期错误)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    # 生产环境只记录日志,不暴露traceback
    logger.error(
        "Unhandled Server Error",
        path=str(request.url.path),
        error_type=type(exc).__name__,
        error_msg=str(exc),
        traceback=traceback.format_exc() if app.debug else None
    )
    return JSONResponse(
        status_code=500,
        content={
            "code": 500,
            "message": "服务器内部错误,请稍后联系管理员",
            "path": str(request.url.path),
            "timestamp": datetime.utcnow().isoformat()
        }
    )

2.2 注意处理器注册顺序

必须先注册「具体异常」,再注册「通用异常」,否则具体处理器永远不会生效:

# ❌ 错误:通用处理器放在前面
@app.exception_handler(Exception)
async def wrong_order(...): ...

@app.exception_handler(ValueError)
async def never_triggered(...): ...

# ✅ 正确
@app.exception_handler(ValueError)
async def value_error(...): ...

@app.exception_handler(Exception)
async def general(...): ...

三、自定义业务异常类

HTTPException 硬编码状态码和错误信息太粗糙,统一的自定义业务异常体系是企业级应用的标配。

3.1 定义核心异常类

创建 exceptions.py 管理所有业务异常:

from fastapi import HTTPException
from typing import Optional, Dict, Any

class AppException(HTTPException):
    """基础业务异常,封装error_code等字段"""
    def __init__(
        self,
        status_code: int = 400,
        message: str = "Bad Request",
        error_code: Optional[str] = None,
        headers: Optional[Dict[str, Any]] = None
    ):
        super().__init__(
            status_code=status_code,
            detail=message,
            headers=headers
        )
        self.app_code = error_code or f"E{status_code}"

class NotFoundException(AppException):
    """资源不存在"""
    def __init__(self, resource: str, identifier: str):
        super().__init__(
            status_code=404,
            message=f"{resource} '{identifier}' 不存在",
            error_code="RESOURCE_NOT_FOUND"
        )

class UnauthorizedException(AppException):
    """未授权"""
    def __init__(self, reason: str = "请先登录"):
        super().__init__(
            status_code=401,
            message=reason,
            error_code="UNAUTHORIZED",
            headers={"WWW-Authenticate": "Bearer"}
        )

class RateLimitException(AppException):
    """请求限流"""
    def __init__(self, retry_after: int = 60):
        super().__init__(
            status_code=429,
            message=f"请求过于频繁,请{retry_after}秒后重试",
            error_code="RATE_LIMIT_EXCEEDED"
        )
        self.retry_after = retry_after

3.2 注册业务异常处理器

专门注册 AppException 的处理器,保留 app_code 等扩展字段:

@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
    content = {
        "code": exc.status_code,
        "app_code": exc.app_code,
        "message": exc.detail,
        "path": str(request.url.path),
        "timestamp": datetime.utcnow().isoformat()
    }
    response = JSONResponse(status_code=exc.status_code, content=content)
    if isinstance(exc, RateLimitException):
        response.headers["Retry-After"] = str(exc.retry_after)
    return response

3.3 在业务逻辑中使用

from exceptions import NotFoundException, UnauthorizedException

@app.delete("/articles/{article_id}")
async def delete_article(article_id: int, current_user: dict = Depends(get_current_user)):
    # 模拟数据库查询
    article = {"id": article_id, "author_id": 1}
    if not article:
        raise NotFoundException("文章", str(article_id))
    if article["author_id"] != current_user["id"]:
        raise UnauthorizedException("无权删除他人文章")
    return {"deleted": article_id, "message": "删除成功"}

四、统一响应格式

不仅是错误响应,成功响应也应该统一格式,方便前端统一处理。

4.1 封装统一响应模型

创建 schemas.py 用 Pydantic 定义响应规范:

from pydantic import BaseModel, Field
from typing import TypeVar, Generic, Optional
from datetime import datetime

T = TypeVar("T")

class ApiResponse(BaseModel, Generic[T]):
    """通用成功响应"""
    code: int = 200
    message: str = "success"
    data: Optional[T] = None
    timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())

class ApiError(BaseModel):
    """通用错误响应"""
    code: int
    app_code: str
    message: str
    path: str
    timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())

# 辅助函数
def success_response(data: Optional[T] = None, message: str = "success") -> ApiResponse[T]:
    return ApiResponse(data=data, message=message)

五、常见陷阱与避坑

❌ 陷阱1:暴露敏感信息

生产环境绝对不要返回 traceback 或数据库查询语句:

# ❌ 错误
@app.exception_handler(Exception)
async def leaky_handler(...):
    return JSONResponse(500, {"traceback": traceback.format_exc()})

# ✅ 正确
@app.exception_handler(Exception)
async def safe_handler(...):
    if app.debug:
        content["traceback"] = traceback.format_exc()
    return JSONResponse(500, content)

❌ 陷阱2:用异常控制正常流程

异常的创建和捕获开销较大,不要用于正常分支判断

# ❌ 错误
def get_fruit_safe(fruit_id: str):
    try:
        return fruits[fruit_id]
    except KeyError:
        return None

# ✅ 正确
def get_fruit_optimized(fruit_id: str):
    return fruits.get(fruit_id)

六、总结

异常类型触发场景处理方式
HTTPException/AppException业务可控错误自定义异常类 + 统一响应格式
RequestValidationErrorPydantic/请求参数自动校验失败格式化错误字段路径 + 统一提示
Exception所有未预期错误生产环境隐藏细节 + 详细日志记录

💡 核心原则:客户端收到可预期的响应,服务端记录可追溯的日志


- [FastAPI 官方异常处理文档](https://fastapi.tiangolo.com/tutorial/handling-errors/) - [Pydantic 自定义验证器](https://docs.pydantic.dev/latest/concepts/validators/) - [Starlette 异常处理机制](https://www.starlette.io/exceptions/)