FastAPI 中全局异常处理

这是整合了全局异常处理 的完整 FastAPI 后端开发流程,涵盖从项目结构、安全配置、数据库、认证逻辑到统一错误响应的全部关键环节,并特别强调安全与可维护性。


🧱 一、项目结构(含全局异常)

复制代码

text

编辑

复制代码
/backend
├── app/
│   ├── __init__.py
│   ├── main.py                 # FastAPI 实例 + 全局异常处理器
│   ├── config.py               # 配置(SECRET_KEY 等)
│   ├── database.py             # DB 会话
│   ├── exceptions.py           # 自定义异常 + 全局处理器
│   ├── models/
│   │   └── user.py
│   ├── schemas/
│   │   └── auth.py, common.py
│   ├── routers/
│   │   └── auth.py
│   └── core/
│       └── security.py
├── alembic/
├── .env
├── .gitignore
└── requirements.txt

⚠️ 二、自定义异常与全局异常处理(核心)

✅ 1. schemas/common.py:统一响应格式

复制代码

python

编辑

复制代码
# app/schemas/common.py
from pydantic import BaseModel
from typing import Optional

class ErrorResponse(BaseModel):
    detail: str
    code: Optional[str] = None  # 可选业务错误码,如 "USER_NOT_FOUND"

✅ 2. exceptions.py:自定义异常 + 全局处理器

复制代码

python

编辑

复制代码
# app/exceptions.py
from fastapi import FastAPI, Request, status
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from sqlalchemy.exc import IntegrityError
from jose.exceptions import JWTError
import logging

logger = logging.getLogger("app")

# 自定义业务异常
class UserNotFoundException(Exception):
    pass

class InvalidCredentialsException(Exception):
    pass

class PasswordTooLongException(Exception):
    pass

def register_exception_handlers(app: FastAPI):
    @app.exception_handler(StarletteHTTPException)
    async def http_exception_handler(request: Request, exc: StarletteHTTPException):
        logger.warning(f"HTTP {exc.status_code}: {exc.detail} | URL: {request.url}")
        return JSONResponse(
            status_code=exc.status_code,
            content={"detail": exc.detail}
        )

    @app.exception_handler(RequestValidationError)
    async def validation_exception_handler(request: Request, exc: RequestValidationError):
        logger.error(f"Validation error: {exc.errors()} | URL: {request.url}")
        return JSONResponse(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            content={"detail": "请求参数格式错误", "errors": exc.errors()}
        )

    @app.exception_handler(IntegrityError)
    async def integrity_error_handler(request: Request, exc: IntegrityError):
        logger.error(f"Database integrity error: {str(exc)}")
        return JSONResponse(
            status_code=status.HTTP_400_BAD_REQUEST,
            content={"detail": "用户名已存在"}
        )

    @app.exception_handler(JWTError)
    async def jwt_error_handler(request: Request, exc: JWTError):
        logger.warning(f"JWT decode error: {str(exc)} | URL: {request.url}")
        return JSONResponse(
            status_code=status.HTTP_401_UNAUTHORIZED,
            content={"detail": "无效的认证凭证"},
            headers={"WWW-Authenticate": "Bearer"}
        )

    # 自定义业务异常
    @app.exception_handler(UserNotFoundException)
    async def user_not_found_handler(request: Request, exc: UserNotFoundException):
        return JSONResponse(
            status_code=status.HTTP_404_NOT_FOUND,
            content={"detail": "用户不存在"}
        )

    @app.exception_handler(InvalidCredentialsException)
    async def invalid_credentials_handler(request: Request, exc: InvalidCredentialsException):
        return JSONResponse(
            status_code=status.HTTP_401_UNAUTHORIZED,
            content={"detail": "用户名或密码错误"},
            headers={"WWW-Authenticate": "Bearer"}
        )

    @app.exception_handler(PasswordTooLongException)
    async def password_too_long_handler(request: Request, exc: PasswordTooLongException):
        return JSONResponse(
            status_code=status.HTTP_400_BAD_REQUEST,
            content={"detail": "密码长度不能超过72字节"}
        )

    # 通用服务器错误(兜底)
    @app.exception_handler(Exception)
    async def global_exception_handler(request: Request, exc: Exception):
        logger.error(f"Unexpected error: {repr(exc)} | URL: {request.url}", exc_info=True)
        return JSONResponse(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            content={"detail": "服务器内部错误,请稍后再试"}
        )

优势

  • 所有异常返回 统一 JSON 格式
  • 敏感信息(如 SQL 错误)不暴露给前端
  • 记录日志便于排查
  • 保留标准 HTTP 状态码(401、404、500 等)

🧩 三、在 main.py 中注册异常处理器

复制代码

python

编辑

复制代码
# app/main.py
from fastapi import FastAPI
from app.routers import auth
from app.exceptions import register_exception_handlers
from app.database import Base, engine

# 创建表(仅开发用,生产用 Alembic)
Base.metadata.create_all(bind=engine)

app = FastAPI(title="Note App API", version="1.0.0")

# 注册全局异常处理器
register_exception_handlers(app)

# 路由
app.include_router(auth.router, prefix="/api/v1")

🔑 四、在业务逻辑中抛出自定义异常(示例)

复制代码

python

编辑

复制代码
# app/routers/auth.py
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from app import models, security
from app.database import get_db
from app.exceptions import InvalidCredentialsException, PasswordTooLongException
from app.schemas import auth as auth_schema

router = APIRouter()

@router.post("/login", response_model=auth_schema.Token)
def login(data: auth_schema.LoginRequest, db: Session = Depends(get_db)):
    user = db.query(models.User).filter(models.User.username == data.username).first()
    
    if not user:
        raise InvalidCredentialsException()  # 统一提示,防用户枚举
    
    if len(data.password.encode('utf-8')) > 72:
        raise PasswordTooLongException()
    
    if not security.verify_password(data.password, user.password):
        raise InvalidCredentialsException()
    
    # ...生成 token...
    return {"access_token": access_token, "token_type": "bearer"}

好处:业务代码干净,错误处理集中


📦 五、依赖更新(requirements.txt

确保包含日志和异常相关包:

复制代码

txt

编辑

复制代码
fastapi==0.115.0
uvicorn[standard]==0.32.0
sqlalchemy==2.0.36
passlib[bcrypt]==1.7.4          # 注意 bcrypt < 4.0
python-jose[cryptography]==3.3.0
python-dotenv==1.0.1
alembic==1.13.2
pydantic-settings==2.6.1

🛡️ 六、安全与健壮性增强点

功能 实现方式
防暴力破解 login 中记录失败次数(Redis + 限流)
敏感日志脱敏 日志中不记录密码、token
CORS 安全 限制 allow_origins=["https://your-frontend.com"]
HTTPS 强制 生产环境用 Nginx 或云服务强制 HTTPS
速率限制 使用 slowapi/login 限流

✅ 七、全局异常处理的优势总结

场景 传统做法 使用全局异常处理
参数校验失败 返回 422,但格式不统一 ✅ 统一返回 {"detail": "...", "errors": [...]}
数据库唯一冲突 报 500,暴露 SQL 错误 ✅ 返回 400:"用户名已存在"
Token 无效 报 500 或模糊错误 ✅ 返回 401 + WWW-Authenticate: Bearer
未捕获异常 崩溃或暴露堆栈 ✅ 返回 500:"服务器内部错误",日志记录详情
业务逻辑错误 手动写 raise HTTPException ✅ 抛自定义异常,自动转为标准响应

🚀 最终效果(前端看到的响应)

登录成功

复制代码

json

编辑

复制代码
{ "access_token": "xxx", "token_type": "bearer" }

密码错误

复制代码

json

编辑

复制代码
{ "detail": "用户名或密码错误" }
// 状态码: 401
// Header: WWW-Authenticate: Bearer

用户名已存在(注册时)

复制代码

json

编辑

复制代码
{ "detail": "用户名已存在" }
// 状态码: 400

服务器崩溃

复制代码

json

编辑

复制代码
{ "detail": "服务器内部错误,请稍后再试" }
// 状态码: 500
// (真实错误记录在后端日志中)

✅ 总结 Checklist

  • 使用 exceptions.py 集中管理所有异常
  • 自定义业务异常(如 InvalidCredentialsException
  • 捕获 IntegrityErrorJWTErrorRequestValidationError 等常见错误
  • 所有响应格式统一(ErrorResponse
  • 敏感错误不暴露细节,只记录日志
  • 保留标准 HTTP 状态码语义
相关推荐
玄同76520 小时前
Python 后端三剑客:FastAPI/Flask/Django 对比与 LLM 开发选型指南
人工智能·python·机器学习·自然语言处理·django·flask·fastapi
张3蜂2 天前
Python 四大 Web 框架对比解析:FastAPI、Django、Flask 与 Tornado
前端·python·fastapi
雪碧聊技术2 天前
ORM简介、安装、使用流程
fastapi·orm·基础代码编写
曲幽2 天前
FastAPI实战:用懒加载与Lifespan优雅管理重型依赖
fastapi·async·lifespan·lazy loading·startup event
雪碧聊技术2 天前
ORM-查询
fastapi·orm·查询
Li emily2 天前
解决港股实时行情数据 API 接入难题
人工智能·python·fastapi
yuezhilangniao2 天前
AI智能体全栈开发工程化规范 备忘 ~ fastAPI+Next.js
javascript·人工智能·fastapi
a1117762 天前
图书借阅管理系统(FastAPI + Vue)
前端·vue.js·fastapi
曲幽3 天前
FastAPI生命周期管理实战:从启动到关闭,如何优雅地管好你的“资源家当”
redis·python·fastapi·web·shutdown·startup·lifespan
极客小云3 天前
【基于AI的自动商品试用系统:不仅仅是虚拟试衣!】
javascript·python·django·flask·github·pyqt·fastapi