一句話先說結論:FastAPI 的依賴注入靠的是“檢查函數簽名”——參數必須用
Depends()包起來,才會被當成依賴去解析,否則就退化成普通 query 參數。三個最常見的坑:漏寫Depends()直接 422、yield依賴的清理沒寫在finally裡(例外時洩漏)、以及預設的同請求緩存讓帶副作用的依賴被“複用”出詭異結果。
背景
FastAPI 的 DI 不是 Spring 那種“裝配整個物件圖”的容器,而是一套很輕量的機制:你聲明端點需要什麼(數據庫會話、當前用戶、設定),框架在請求時自動調用對應的函數,把返回值注入進來。理解這套“簽名驅動”的解析方式,是避開大部分坑的前提。
現象
幾種典型報錯。
漏寫 Depends() 時,依賴參數被降級成普通 query 參數,FastAPI 返回 422,響應體:
{
"detail": [
{
"type": "missing",
"loc": ["query", "current_user"],
"msg": "Field required"
}
]
}
async 依賴配了 def(或依賴寫成循環),會直接拋運行時例外。報錯原文:
RuntimeError: Dependency 'get_current_user' is a coroutine function but it is not wrapped with async def in the function signature.
完整堆疊:
Traceback (most recent call last):
File "/usr/local/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 428, in run_asgi
result = await self.app(scope, receive, send)
File "/usr/local/lib/python3.10/site-packages/starlette/routing.py", line 680, in __call__
await route.handle(scope, receive, send)
File "/usr/local/lib/python3.10/site-packages/fastapi/routing.py", line 227, in app
solved_result = await solve_dependencies(request=request, dependant=dependant, body=body)
File "/usr/local/lib/python3.10/site-packages/fastapi/dependencies/utils.py", line 606, in solve_dependencies
raise RuntimeError(
RuntimeError: Dependency 'get_current_user' is a coroutine function but it is 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()。
