一句话先说结论: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依赖:清理放finally,except后记得raise。- 默认同请求缓存依赖结果,带副作用的依赖用
use_cache=False。 - 测试 override 用同一个函数对象做 key,测完
clear()。