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 状态码语义
相关推荐
dangfulin4 小时前
fastapi+sqlalchemy实现一对一、一对多、多对多关系数据操作
fastapi
龙腾AI白云8 小时前
【基于Transformer的人工智能模型搭建与fine-tuning】
scikit-learn·fastapi
叼奶嘴的超人1 天前
手动创建Docker版Fastapi CI/CD镜像文件
ci/cd·docker·fastapi
regret~1 天前
【笔记】Ant Design+FastAPI 项目 Linux 服务器内网部署完整笔记
服务器·笔记·fastapi
regret~1 天前
【笔记】Ant Design(含Umi Max)+FastAPI 内网部署&接口代理 核心笔记
笔记·fastapi
全栈测试笔记2 天前
FastAPI系列(12):响应模型参数
开发语言·python·fastapi
叼奶嘴的超人2 天前
Fastapi之UV安装方式与使用方式
fastapi·uv
强化试剂瓶3 天前
Silane-PEG8-DBCO,硅烷-聚乙二醇8-二苯并环辛炔技术应用全解析
python·flask·numpy·pyqt·fastapi
曲幽3 天前
FastAPI日志实战:从踩坑到优雅配置,让你的应用会“说话”
python·logging·fastapi·web·error·log·info
布局呆星4 天前
FastAPI:高性能Python Web框架
fastapi