前言:作为FastAPI新手,最近开发登录功能时,踩了一堆关于“全局异常处理”的坑——不知道全局异常到底有啥用、没它行不行,甚至写了异常处理器,还疑惑“我只抛了个HTTPException,它怎么就能拿到状态码和提示?”。今天把这些新手必懵的问题,结合我自己的实战代码,一次性讲透,全程接地气,不搞虚的,新手直接对号入座!

一、先交代背景:我正在做啥?

目前在写一个简单的“随机事项查询”项目,核心功能:用户注册+登录,登录后根据分类(play/study/eat等)查询随机事项。技术栈:FastAPI + SQLAlchemy 2.0(Mapped正规写法) + 密码加密,全程异步开发。

就是在写登录、注册接口时,被“全局异常处理”搞懵了,才有了下面这些疑问和总结,完全贴合新手实战场景,不是纯理论!

二、新手最懵的3个问题(我全踩过)

问题1:没有全局异常处理器,不能抛HTTPException吗?

这是我最开始的核心疑问!答案:完全可以抛!

举个例子,我写登录接口时,判断用户名不存在,直接抛异常:

raise HTTPException(status_code=400, detail="用户名不存在")

没有全局异常处理器的情况下,程序不会崩,但是前端收到的返回格式是FastAPI默认的,特别简陋:

 {"detail": "用户名不存在" }

没有code、没有message,前端对接起来特别麻烦,而且每个接口报错,都要手动拼返回格式,重复代码超多!

问题2:全局异常处理器到底干了啥?能修复代码bug吗?

我之前一直以为,有了全局异常,代码就不会报错了——大错特错!

重点强调:全局异常处理器不修复任何bug!

它的核心作用只有2个,特别简单:

  1. 拦截你手动抛的HTTPException,把默认的简陋格式,改成咱们想要的统一格式(比如code+message);

  2. 拦截程序意外崩溃(比如变量未定义、数据库连不上),不把后端的报错堆栈暴露给前端,包装成友好的500错误。

举个通俗的例子:全局异常就像一个“错误包装工”,不管是你主动抛的错,还是程序不小心崩了,它都能把错误打包成统一的“快递”(JSON格式)发给前端,不让前端看到杂乱的“原材料”(默认报错格式、报错堆栈)。

问题3:我只抛了HTTPException(400, detail="xxx"),异常处理器怎么拿到status_code?

这是最细节、最懵的一个点!我当时写这段代码时,盯着exc.status_code看了半天,不知道它从哪来的:

async def http_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,  # 这个exc.status_code从哪来?
        content={"code": exc.status_code, "message": exc.detail},
    )

答案:FastAPI自动帮你把参数装进去了!

当你写raise HTTPException(400, detail="用户名不存在")时,FastAPI底层会自动创建一个exc对象,把你传的400和“用户名不存在”,分别赋值给exc.status_code和exc.detail。

你看不见这个过程,但exc对象是真实存在的,所以异常处理器里,直接用exc.status_code、exc.detail就能拿到值,不用你手动传任何参数!

简单说:你负责“抛错”(告诉程序错在哪、错的状态码),FastAPI负责“打包”(把错误信息装进exc),全局异常处理器负责“美化”(把exc里的信息,改成统一格式返回)。

三、实战代码:从无全局异常,到有全局异常的完整对比

结合我自己的登录、注册接口,给大家看最直观的对比,新手可以直接复制用。

先准备基础依赖(必装)

密码加密需要用到passlib和bcrypt,先安装:

pip install passlib[bcrypt]

1. 无全局异常:登录接口(繁琐、格式乱)

没有全局异常时,每个接口都要手动拼错误返回,不然前端拿到的格式不统一:

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from config.things import get_async_db
from crud.numbers import get_number_by_username
from schemas.numbers import NumberCreate
from utils.security import verify_password

router = APIRouter(prefix="/numbers", tags=["numbers"])

# 登录接口(无全局异常,手动拼错误返回)
@router.post("/login")
async def login_number_endpoint(
    number_data: NumberCreate, 
    db: AsyncSession = Depends(get_async_db)
):
    # 查找用户
    db_number = await get_number_by_username(db, number_data.username)
    # 手动判错,手动拼返回格式
    if not db_number:
        return {"code": 400, "message": "用户名不存在", "data": None}
    if not verify_password(number_data.password, db_number.password):
        return {"code": 400, "message": "用户名或密码错误", "data": None}
    # 登录成功返回
    return {"code": 200, "message": "Login successful", "user_id": db_number.id}

缺点:每个接口都要写return错误,重复代码多,万一忘了写,返回格式就乱了。

2. 有全局异常:登录接口(简洁、格式统一)

第一步:写全局异常处理器(新建exception.py)

from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse

# 处理主动抛出的HTTPException(比如用户名不存在、密码错误)
async def http_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content={"code": exc.status_code, "message": exc.detail},
    )

# 兜底处理所有未知异常(比如代码bug、数据库报错)
async def global_exception_handler(request: Request, exc: Exception):
    return JSONResponse(
        content={"code": 500, "message": "服务器内部错误", "data": None},
        status_code=500
    )

第二步:在main.py注册全局异常

from fastapi import FastAPI
from exception import http_exception_handler, global_exception_handler
from fastapi import HTTPException

app = FastAPI()

# 注册全局异常,全局生效
app.add_exception_handler(HTTPException, http_exception_handler)
app.add_exception_handler(Exception, global_exception_handler)

第三步:简化登录接口(不用手动拼错误返回)

@router.post("/login")
async def login_number_endpoint(
    number_data: NumberCreate, 
    db: AsyncSession = Depends(get_async_db)
):
    db_number = await get_number_by_username(db, number_data.username)
    # 直接抛异常,不用手动return
    if not db_number:
        raise HTTPException(status_code=400, detail="用户名不存在")
    if not verify_password(number_data.password, db_number.password):
        raise HTTPException(status_code=400, detail="用户名或密码错误")
    return {"code": 200, "message": "Login successful", "user_id": db_number.id}

效果:抛异常后,全局异常处理器自动拦截,前端收到统一格式:

{
  "code": 400,
  "message": "用户名不存在"
}

再也不用手动拼错误返回,代码简洁太多!

四、新手必记的核心总结(避坑关键)

  1. 有无全局异常,都能使用raise HTTPException,区别只是返回格式不同;

  2. 全局异常不修复bug,只统一错误返回格式、隐藏服务器内部报错;

  3. exc.status_code、exc.detail不是凭空来的,是FastAPI自动把你抛异常时传的参数装进去的;

  4. 密码一定要加密存储(用本文的bcrypt算法),禁止明文存数据库,这是企业级规范;

  5. 全局异常处理器必须在main.py注册,否则不生效!

五、最后说两句

作为新手,刚开始接触FastAPI的全局异常,真的很容易懵——不知道它的作用、不知道参数从哪来、不知道怎么用。但只要结合实际代码,多试几次,就会发现它特别简单,核心就是“统一格式、简化代码”。

本文所有代码,都是我自己实战用过的,没有任何多余的理论,新手可以直接复制到自己的项目里,替换成自己的数据库配置和字段,就能直接用。后续还会更新登录权限控制、前端对接等内容,感兴趣的可以关注~

如果有和我一样的新手,对全局异常、登录接口还有疑问,欢迎在评论区交流,一起避坑、一起进步!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐