跳到主要内容

06 错误处理

本章目标:学会向客户端返回规范的错误信息,以及全局统一处理异常。

1. HTTPException:主动返回错误

业务中遇到「用户不存在」「余额不足」等情况时,抛出 HTTPException

from fastapi import FastAPI, HTTPException, status

app = FastAPI()

fake_db = {1: {"id": 1, "name": "小明"}}


@app.get("/users/{user_id}")
def get_user(user_id: int):
if user_id not in fake_db:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="用户不存在",
)
return fake_db[user_id]

访问 /users/999 会收到:

HTTP 404
{"detail": "用户不存在"}

要点:

  • raise 不是 return!抛出异常后函数立即结束
  • detail 可以是字符串,也可以是字典/列表(会原样放进响应)
  • 在函数的任何深度抛出都有效(包括被调用的子函数里),FastAPI 会接住它

2. 常见业务错误示例

# 参数合法但业务上不允许 → 400
if user.balance < price:
raise HTTPException(status_code=400, detail="余额不足")

# 未登录 → 401
raise HTTPException(status_code=401, detail="请先登录")

# 登录了但没权限 → 403
raise HTTPException(status_code=403, detail="无权访问该资源")

# 资源不存在 → 404
raise HTTPException(status_code=404, detail="文章不存在")

# 资源冲突(如用户名已被占用)→ 409
raise HTTPException(status_code=409, detail="用户名已存在")

分不清用哪个时的口诀:没登录 401,没权限 403,找不到 404,其他客户端问题 400

3. 理解 422:FastAPI 的自动校验错误

当请求数据不符合你声明的类型/模型时,FastAPI 自动返回 422,格式固定:

{
"detail": [
{
"type": "int_parsing",
"loc": ["path", "user_id"],
"msg": "Input should be a valid integer...",
"input": "abc"
}
]
}
  • loc:错误位置,["path", "user_id"] 表示路径参数 user_id,["body", "email"] 表示请求体的 email 字段
  • msg:人类可读的原因

你不需要为 422 写任何代码,但要学会读懂它——前端联调时对方问「为什么报 422」,你要能看 locmsg 快速定位。

4. 自定义异常 + 全局异常处理器

项目变大后,到处写 raise HTTPException(404, "xx不存在") 很啰嗦。更优雅的方式是:定义业务异常类,用异常处理器统一转成 HTTP 响应。

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

app = FastAPI()


# 1. 定义业务异常
class NotFoundError(Exception):
def __init__(self, resource: str):
self.resource = resource


# 2. 注册处理器:所有地方抛出的 NotFoundError 都会走到这里
@app.exception_handler(NotFoundError)
def not_found_handler(request: Request, exc: NotFoundError):
return JSONResponse(
status_code=404,
content={"detail": f"{exc.resource}不存在"},
)


# 3. 业务代码里只管抛,不关心 HTTP 细节
@app.get("/articles/{article_id}")
def get_article(article_id: int):
article = None # 假装查了数据库没查到
if article is None:
raise NotFoundError("文章")
return article

好处:业务代码和 HTTP 细节解耦,错误响应格式全项目统一。

5. 兜底:处理所有未捕获的异常

代码有 bug 抛了意料之外的异常时,FastAPI 默认返回 500 和 Internal Server Error。你可以加一个兜底处理器,统一格式并记录日志:

import logging

logger = logging.getLogger("app")


@app.exception_handler(Exception)
def unhandled_exception_handler(request: Request, exc: Exception):
# 关键:把异常记录下来,否则线上出问题无从排查
logger.exception(f"未处理异常: {request.method} {request.url}")
return JSONResponse(
status_code=500,
content={"detail": "服务器内部错误,请稍后重试"},
)

开发阶段建议先不加这个——让异常直接在终端打印完整堆栈,更方便调试。上线前再加上。

6. 统一响应格式(可选风格)

有些团队喜欢所有接口都返回统一包装:

{"code": 0, "message": "ok", "data": {...}}

FastAPI 不强制任何风格。本教程采用更贴近 HTTP 语义的风格(用状态码区分成败、错误放 detail),这也是 FastAPI 社区的主流做法。你入职后跟随团队规范即可,机制都是本章讲的这些。

本章小结

  • 业务错误用 raise HTTPException(status_code=..., detail=...)
  • 422 是自动校验错误,学会读 loc 定位问题
  • 大项目用「自定义异常 + @app.exception_handler」统一错误处理
  • 生产环境要有兜底异常处理器并记录日志

下一章:07-依赖注入 —— FastAPI 最重要的机制,也是连接数据库的关键。