DevFix
FastAPI已验证

FastAPI Depends 依赖注入的坑:忘写 Depends、yield 清理与 use_cache

@debug_master更新于 3 天前阅读 6 min0

一句话先说结论:FastAPI 的依赖注入靠的是“检查函数签名”——参数必须用 Depends() 包起来,才会被当成依赖去解析,否则就退化成普通 query 参数。三个最常见的坑:漏写 Depends() 直接 422、yield 依赖的清理没写在 finally 里(异常时泄漏)、以及默认的同请求缓存让带副作用的依赖被“复用”出诡异结果。

背景

FastAPI 的 DI 不是 Spring 那种“装配整个对象图”的容器,而是一套很轻量的机制:你声明端点需要什么(数据库会话、当前用户、配置),框架在请求时自动调用对应的函数,把返回值注入进来。理解这套“签名驱动”的解析方式,是避开大部分坑的前提。

现象

几种典型报错:

// 忘了 Depends(),依赖参数被当成了 query 参数
422 Unprocessable Entity
{
  "detail": [{"loc": ["query", "current_user"], "msg": "field required"}]
}
// async 依赖却用了 def + yield,或依赖循环
RuntimeError: Dependency 'get_current_user' is a coroutine function
but it's not wrapped with async def in the function signature.

还有一种最隐蔽的:依赖偷偷返回了 None,路由里接着调 user.id,报的却是看起来跟依赖无关的 AttributeError: 'NoneType' object has no attribute 'id'

根因分析

坑 1:漏写 Depends()

FastAPI 判断一个参数是不是依赖,全靠 Depends(...) 这个标记。不包,它就是个普通请求参数:

# 错:缺少 Depends,get_user 被当成 query 参数,触发 422
@app.get("/items")
def read_items(user=get_user):
    return user

# 对
@app.get("/items")
def read_items(user=Depends(get_user)):
    return user

坑 2:yield 依赖清理没进 finally,或 except 忘了 raise

yield 依赖是“上下文管理器”,yield 之后的代码在响应返回后跑。要保证异常时也能清理,必须用 finally

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

还有两个容易被忽略的细节:

  • yield 依赖里用 except 捕获异常后,必须再 raise,否则异常被你吞掉,接口会返回一个“看似成功”的结果而不是正确的错误。
  • async 生成器依赖要用 async def + yield,不能写成 def + yield

坑 3:默认缓存让副作用依赖“复用”

FastAPI 默认在同一次请求内缓存依赖结果:同一个依赖被多个地方 Depends 时,只会执行一次、共用同一实例。

@app.get("/profile")
async def profile(
    user1=Depends(get_current_user),
    user2=Depends(get_current_user),
):
    assert user1 is user2   # True,同一实例

如果某个依赖带副作用(比如生成随机数、读取实时状态),这种复用会带来意外。要每次执行,用 use_cache=False

@app.get("/data")
async def data(fresh=Depends(get_fresh_data, use_cache=False)):
    return fresh

坑 4:测试 override 用的 key 必须是同一个函数对象

依赖替换 app.dependency_overrides[get_db] = lambda: mock_session,key 必须是你模块里导入的那个确切函数对象,不是重新 import 或拷贝的。否则就悄悄不生效。测完记得 app.dependency_overrides.clear(),避免污染其他用例。

解决方案

标准的数据库依赖(yield + finally + 提交/回滚)

from typing import Generator

def get_db() -> Generator:
    db = SessionLocal()
    try:
        yield db
        db.commit()          # 成功才提交
    except Exception:
        db.rollback()        # 异常回滚
        raise                # 一定要重新抛出
    finally:
        db.close()           # 一定关闭

子依赖链(认证 → 角色 → 数据)

def get_current_user(token: str = Depends(oauth2_scheme), db=Depends(get_db)):
    user = db.query(User).filter(User.token == token).first()
    if not user:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user

def get_admin_user(user=Depends(get_current_user)):
    if not user.is_admin:
        raise HTTPException(status_code=403, detail="Not authorized")
    return user

依赖可以任意嵌套成树,每个 yield 的退出代码会按正确顺序执行。

别让依赖返回 None

依赖里凡是可能走到“不返回”的分支,都要显式处理,否则下一步的 AttributeError 会把你引到错误的方向:

def get_user(user_id: int, db=Depends(get_db)):
    user = db.query(User).get(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")  # 显式抛出,别返回 None
    return user

小结

  • 依赖参数一定包 Depends(),否则变 query 参数报 422。
  • yield 依赖:清理放 finallyexcept 后记得 raise
  • 默认同请求缓存依赖结果,带副作用的依赖用 use_cache=False
  • 测试 override 用同一个函数对象做 key,测完 clear()

来源

最后更新于 2026-08-22

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法