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 状态码语义
相关推荐
Good kid.4 小时前
【原创】基于 RoBERTa 的智能垃圾分类系统(规则 + AI 混合,FastAPI 接口 + Web Demo)
人工智能·分类·fastapi
博客胡19 小时前
Python-fastAPI的学习与使用
学习·fastapi·ai编程
wang60212521819 小时前
FastAPI中的异步任务执行-celery
fastapi·celery
宁雨桥1 天前
多引擎中英翻译API搭建与使用教程
python·fastapi·翻译
Luke Ewin1 天前
基于FunASR开发的可私有化部署的语音转文字接口 | FunASR接口开发 | 语音识别接口私有化部署
人工智能·python·语音识别·fastapi·asr·funasr
钱彬 (Qian Bin)2 天前
项目实践11—全球证件智能识别系统(切换为PostgreSQL数据库)
人工智能·qt·fastapi
wang6021252182 天前
FastAPI的异步开发-Asyncio
python·fastapi·asyncio
Hi_kenyon3 天前
FastAPI+VUE3创建一个项目的步骤模板(二)
python·fastapi
坚定信念,勇往无前3 天前
python的fastapi+uvicorn的linux离线部署
fastapi