FastAPI 系列

  1. FastAPI 系列:从第一个接口到生产部署

  2. FastAPI 为什么适合做 JSON API

  3. 第一个接口怎么跑起来

  4. 路径、查询和请求体为什么要分开声明

  5. Pydantic 模型怎么当契约

  6. 路由怎么拆

  7. 依赖注入到底省了什么

  8. 数据库会话怎么进接口

  9. 状态码和异常为什么不要裸 raise(当前篇)

  10. 鉴权怎么接到 Depends 上

  11. 中间件、CORS 和后台任务分别解决什么

  12. 测试怎么写才不连真实库

  13. 生产怎么部署

客户端不读你的 traceback。它们读状态码和 JSON detail。裸 raise Exception("没找到") 会变成 500,还会把内部信息打进日志甚至响应。业务失败要翻译成 HTTPException 或注册过的异常处理器。

路径异常经 HTTPException 或 handler 变成 JSON 响应

用 HTTPException 表达协议错误

from fastapi import FastAPI, HTTPException, status

app = FastAPI()


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    item = store.get(item_id)
    if item is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Item not found",
        )
    return item

401 还要带 headers={"WWW-Authenticate": "Bearer"},浏览器和部分客户端才知道走认证。403 是「身份已知但不允许」,不要和 401 混用。

校验错误已经有 422

Pydantic 失败由框架处理,不要在路由里把模型校验再包一层 400。400 留给「语法对、业务不允许」,例如库存不足。

状态码

典型含义

400

请求语义不合法,但 JSON 结构是对的

401

未认证或 Token 无效

403

已认证,无权限

404

资源不存在

409

冲突,例如唯一键重复

422

字段校验失败

500

未处理的服务器错误,不应作为业务分支

领域异常映射到 HTTP

class ItemNotFoundError(Exception):
    def __init__(self, item_id: int):
        self.item_id = item_id


@app.exception_handler(ItemNotFoundError)
async def item_not_found_handler(request, exc: ItemNotFoundError):
    return JSONResponse(
        status_code=404,
        content={"detail": f"Item {exc.item_id} not found"},
    )

服务层抛领域异常,Web 层翻译。这样服务层可以给 CLI 或后台任务复用,不必依赖 FastAPI。

不要把内部异常细节返回给客户端

生产关闭 traceback 响应。日志可以记完整堆栈,响应只给稳定的 detail。数据库报错原文、文件路径、SQL 都不要出现在 JSON 里。

常见误区

用 200 包装错误

{"code": 404, "msg": "没有"} 配 HTTP 200,会让网关、监控和 TestClient 全部失效。HTTP 状态码是协议的一部分。

catch Exception 后静默返回空对象

故障被吞掉,监控看不到。未知错误应成为 500 并报警。

404 和 403 用同一个文案

对隐私敏感的资源,有时故意统一成 404,避免探测是否存在。这是产品决策,要写进接口约定,而不是随手混用。

小结

状态码是给机器读的契约。业务失败走 HTTPException 或 handler,系统失败才是 500。下一篇把 401/403 接到 Depends 上的 JWT。

参考资料