本文 AI 產出,尚未審核

FastAPI 教學:請求生命週期 – Request 狀態保存 (request.state)


簡介

FastAPI 中,每一次 HTTP 請求都會經過一條明確的生命週期:從路由匹配 → 中間件(middleware) → 依賴注入(dependency) → 路由函式 → 回傳 Response。開發者往往只關注「路由函式」本身,卻忽略了在 請求前後 想要暫存或共享資料的需求。

request.state 正是為了這個目的而設計的,它提供了一個 輕量級、類似屬性的容器,讓你在同一個請求的不同階段之間安全地傳遞自訂資訊,而不必依賴全域變數或複雜的上下文管理。

掌握 request.state 不僅能讓程式碼更乾淨、可測試,還能在 日誌追蹤、權限驗證、資料庫交易管理 等實務情境中發揮關鍵作用。本章節將從概念說明、實作範例到常見陷阱,逐步帶你熟悉這項功能。


核心概念

1. request.state 是什麼?

  • requestStarlette(FastAPI 底層框架)提供的 Request 物件。
  • stateRequest 上的一個 空的 SimpleNamespace,預設沒有任何屬性。
  • 你可以隨意在 state動態添加屬性(例如 request.state.user = user_obj),這些屬性會在同一次請求的整條流程中保持可用。

注意state 的生命週期僅限於單一請求,請求結束後會被自動回收,避免了記憶體泄漏的風險。

2. 為什麼不直接使用全域變數或 Depends

方法 優點 缺點
全域變數 直接、簡單 共享跨請求、非執行緒安全、測試困難
依賴注入 (Depends) FastAPI 原生支援 每次呼叫都會重新產生,無法在中間件與路由間共享同一個實例
request.state 請求範圍、輕量、易於測試 必須在 Request 物件可取得的地方使用(如中間件、路由、依賴)

3. 何時使用 request.state

  • 跨層級資料傳遞:例如在自訂認證中間件取得使用者資訊,之後在路由或其他依賴中直接讀取。
  • 一次請求的臨時緩存:如把計算結果暫存於 state,避免重複運算。
  • 請求唯一識別碼:在中間件產生 request_id,供日誌、錯誤回報統一使用。

程式碼範例

以下示範 5 個常見且實用的 request.state 用法,每段程式碼都附有說明註解,方便你直接搬移到專案中。

範例 1️⃣:在 Middleware 中設定使用者資訊

# main.py
from fastapi import FastAPI, Request, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

app = FastAPI()
security = HTTPBearer()

# 假設有一個簡單的驗證函式
def fake_decode_token(token: str) -> dict:
    # 這裡僅示範,實務上請使用 JWT 或 OAuth2
    if token == "valid-token":
        return {"username": "alice", "role": "admin"}
    raise HTTPException(status_code=401, detail="Invalid token")

@app.middleware("http")
async def auth_middleware(request: Request, call_next):
    # 從 Header 取出 Bearer token
    credentials: HTTPAuthorizationCredentials = await security(request)
    payload = fake_decode_token(credentials.credentials)

    # 把使用者資訊放入 request.state
    request.state.user = payload          # <-- 重要
    response = await call_next(request)
    return response

@app.get("/me")
async def read_me(request: Request):
    # 直接從 state 讀取使用者資訊
    user = request.state.user
    return {"username": user["username"], "role": user["role"]}

重點:中間件只負責「驗證」與「寫入」state,路由只負責「讀取」與「回傳」,達成職責分離。


範例 2️⃣:在 Dependency 中取得 request.state

from fastapi import Depends, Request

def get_current_user(request: Request):
    # 若中間件未設定,則直接拋出錯誤
    if not hasattr(request.state, "user"):
        raise HTTPException(status_code=401, detail="User not authenticated")
    return request.state.user

@app.get("/admin")
async def admin_panel(current_user: dict = Depends(get_current_user)):
    if current_user["role"] != "admin":
        raise HTTPException(status_code=403, detail="Forbidden")
    return {"msg": f"Welcome admin {current_user['username']}!"}

透過 依賴注入 讀取 state,讓路由函式保持乾淨且易於單元測試。


範例 3️⃣:為每個請求產生唯一的 Request ID(日誌追蹤)

import uuid
import logging
from fastapi import Request

logger = logging.getLogger("uvicorn.access")

@app.middleware("http")
async def request_id_middleware(request: Request, call_next):
    request_id = str(uuid.uuid4())
    request.state.request_id = request_id          # <-- 設定唯一 ID

    # 在 log 中加入 request_id,方便追蹤
    logger.info(f"START request_id={request_id} path={request.url.path}")

    response = await call_next(request)

    logger.info(f"END   request_id={request_id} status_code={response.status_code}")
    # 把 request_id 放在回應 Header,前端也能看到
    response.headers["X-Request-ID"] = request_id
    return response

實務技巧:將 request_id 加入日誌與回應 Header,讓前端與後端的除錯流程同步。


範例 4️⃣:在 state 中保存資料庫 Session(SQLAlchemy)

from sqlalchemy.orm import Session
from .database import SessionLocal   # 假設已建立 SessionLocal

@app.middleware("http")
async def db_session_middleware(request: Request, call_next):
    # 建立 DB session,放入 state
    request.state.db: Session = SessionLocal()
    try:
        response = await call_next(request)
        request.state.db.commit()          # 成功則 commit
    except Exception:
        request.state.db.rollback()        # 發生例外則 rollback
        raise
    finally:
        request.state.db.close()           # 確保資源釋放
    return response

def get_db(request: Request) -> Session:
    return request.state.db

@app.get("/items/{item_id}")
async def read_item(item_id: int, db: Session = Depends(get_db)):
    # 直接使用已存在的 session
    item = db.query(Item).filter(Item.id == item_id).first()
    return {"id": item.id, "name": item.name}

使用 state 管理 同一個請求的 DB session,可以避免在每個依賴中重複建立連線,提升效能與一致性。


範例 5️⃣:在 state 中暫存計算結果,避免重複運算

@app.get("/expensive")
async def expensive_calculation(request: Request):
    # 若已計算過,直接回傳快取結果
    if hasattr(request.state, "expensive_result"):
        return {"result": request.state.expensive_result, "cached": True}

    # 假設這是一個耗時的計算
    result = sum(i * i for i in range(10_000_000))
    request.state.expensive_result = result   # 暫存於 state
    return {"result": result, "cached": False}

適用情境:同一個請求內部可能會呼叫多個子函式,而這些子函式需要共享一次性的計算結果。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記檢查屬性是否存在 直接使用 request.state.xxx 會在屬性未設定時拋 AttributeError 使用 hasattr(request.state, "xxx") 或在中間件保證一定會設定。
在非請求上下文使用 request.state 例如在背景任務或啟動事件中直接取 request,會因為沒有 Request 物件而失敗。 僅在能取得 Request 的函式(middleware、路由、依賴)內使用;若需跨請求共享,改用外部快取(Redis、memory cache)。
把大量資料放入 state state 並非設計來存放大型物件,會增加記憶體佔用與 GC 壓力。 僅存放 輕量短暫 的資料(字串、ID、簡單 dict)。大型物件建議使用依賴注入或外部快取。
在多執行緒環境下修改同一屬性 雖然每個請求都有獨立的 state,但若在同一請求內部使用多執行緒(如 ThreadPoolExecutor)共享 state,仍需自行確保執行緒安全。 使用 asyncio.Lock 或將資料傳遞給子執行緒的參數,而非直接讀寫 state
忘記在 finally 釋放資源 如 DB session、文件句柄等若只在成功路徑釋放,例外時會泄漏。 必須finally 區塊中關閉或回收資源(如範例 4 所示)。

最佳實踐小結

  1. 在 Middleware 中寫入,在 Dependency / 路由 中讀取。
  2. 只存放輕量資料(如 ID、旗標、簡易 dict)。
  3. 使用 hasattr 防止 AttributeError
  4. 確保資源在 finally 釋放(DB、檔案、外部連線)。
  5. 將跨請求需求交給外部快取(Redis、Memcached),state 僅限單請求。

實際應用場景

  1. 分散式追蹤(Distributed Tracing)

    • 在入口 Middleware 產生 trace_id,存入 request.state.trace_id,在每個子服務呼叫的 Header 中加入此 ID,最後在日誌或 APM 平台上關聯同一筆請求。
  2. 多租戶(Multi‑Tenant)系統

    • 透過租戶識別 token 在 Middleware 中解析租戶代碼,寫入 request.state.tenant_id,之後所有 DB 查詢或快取操作皆依此租戶 ID 自動切換。
  3. 自訂速率限制(Rate Limiting)

    • 在 Middleware 中根據 IP 或使用者 ID 計算剩餘配額,將結果放入 request.state.rate_limit,在路由內直接回傳或拋出 429。
  4. 臨時 Feature Flag

    • 讀取外部設定服務(如 LaunchDarkly)後,把開關結果寫入 request.state.features,其他模組只要檢查 state.features 即可決定行為。
  5. 請求級別的快取

    • 在一次請求內的多個子路由或子函式需要相同的遠端 API 結果,使用 state 暫存一次回傳,避免重複網路呼叫。

總結

  • request.stateFastAPI/Starlette 提供的請求範圍容器,讓你在 同一個請求的不同階段 安全地傳遞自訂資訊。
  • 透過 Middleware 寫入、Dependency / 路由 讀取的模式,能保持程式碼的職責分離與可測試性。
  • 使用時注意 屬性存在檢查、資源釋放、資料輕量化,避免常見的陷阱。
  • 日誌追蹤、認證授權、資料庫交易、分散式追蹤、Feature Flag 等實務情境中,request.state 能大幅簡化開發流程與提升系統可觀測性。

掌握了 request.state,你的 FastAPI 應用將更具彈性、可維護性,同時也更容易在大型專案或微服務架構中保持一致的請求上下文管理。快把這些範例搬進你的專案,體驗 乾淨、可測、易除錯 的開發體驗吧!