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 状态码语义
相关推荐
曲幽17 小时前
FastAPI压力测试实战:Locust模拟真实用户并发及优化建议
python·fastapi·web·locust·asyncio·test·uvicorn·workers
曲幽2 天前
FastAPI实战:打造本地文生图接口,ollama+diffusers让AI绘画更听话
python·fastapi·web·cors·diffusers·lcm·ollama·dreamshaper8·txt2img
曲幽3 天前
我用FastAPI接ollama大模型,差点被asyncio整崩溃(附对话窗口实战)
python·fastapi·web·async·httpx·asyncio·ollama
闲云一鹤4 天前
Python 入门(二)- 使用 FastAPI 快速生成后端 API 接口
python·fastapi
曲幽4 天前
FastAPI + Ollama 实战:搭一个能查天气的AI助手
python·ai·lora·torch·fastapi·web·model·ollama·weatherapi
百锦再5 天前
Django实现接口token检测的实现方案
数据库·python·django·sqlite·flask·fastapi·pip
Li emily5 天前
解决了股票实时数据接口延迟问题
人工智能·fastapi
司徒轩宇5 天前
FastAPI + Uvicorn 深度理解与异步模型解析
fastapi
郝学胜-神的一滴6 天前
FastAPI:Python 高性能 Web 框架的优雅之选
开发语言·前端·数据结构·python·算法·fastapi
yaoty7 天前
Python日志存储:从单机同步到分布式异步的7种方案
fastapi·日志·logger