跳轉到

FastAPI 中間件最佳實踐

本章節將介紹 FastAPI 中間件開發和使用的最佳實踐,幫助開發者構建更加健壯、高效的應用。

中間件設計原則

單一職責原則

每個中間件應該只負責一個明確的功能,避免混合多種不相關的邏輯:

# 不推薦:混合多種功能的中間件
@app.middleware("http")
async def mixed_middleware(request: Request, call_next):
    # 記錄請求 + 身份驗證 + 請求限流 + 修改響應...
    # 太多職責在一個中間件中!

# 推薦:分離關注點的中間件
@app.middleware("http")
async def logging_middleware(request: Request, call_next):
    print(f"Request: {request.method} {request.url.path}")
    return await call_next(request)

@app.middleware("http")
async def auth_middleware(request: Request, call_next):
    # 只處理身份驗證
    pass

@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    # 只處理速率限制
    pass

可配置性

設計中間件時,應該考慮其可配置性,使其能夠適應不同的使用場景:

class ConfigurableMiddleware(BaseHTTPMiddleware):
    def __init__(
        self, 
        app,
        include_paths: List[str] = None,
        exclude_paths: List[str] = None,
        include_methods: List[str] = None,
        debug_mode: bool = False
    ):
        super().__init__(app)
        # 將所有配置保存為實例變量
        self.include_paths = self._compile_patterns(include_paths)
        self.exclude_paths = self._compile_patterns(exclude_paths)
        self.include_methods = [m.upper() for m in (include_methods or [])]
        self.debug_mode = debug_mode

    # 其餘實現...

可測試性

將核心邏輯與框架集成分離,便於單元測試:

# 將核心邏輯與中間件框架分離
class RateLimiter:
    def __init__(self, limit: int = 100, window: int = 60):
        self.limit = limit
        self.window = window
        self.requests = {}

    def is_rate_limited(self, client_ip: str, current_time: float) -> bool:
        # 核心限流邏輯
        pass

# 使用核心邏輯的中間件
class RateLimitMiddleware(BaseHTTPMiddleware):
    def __init__(self, app, limiter: RateLimiter = None, **kwargs):
        super().__init__(app)
        self.limiter = limiter or RateLimiter(**kwargs)

    async def dispatch(self, request: Request, call_next):
        # 使用獨立的限流器
        pass

性能優化

避免阻塞操作

在中間件中應避免任何阻塞操作,以免影響整個應用的性能:

# 不推薦:在中間件中使用阻塞操作
@app.middleware("http")
async def blocking_middleware(request: Request, call_next):
    time.sleep(1)  # 阻塞整個事件循環!

    # 阻塞 I/O
    with open("log.txt", "a") as f:
        f.write(f"{request.method} {request.url.path}\n")

    return await call_next(request)

# 推薦:使用非阻塞操作
@app.middleware("http")
async def non_blocking_middleware(request: Request, call_next):
    await asyncio.sleep(1)  # 非阻塞

    # 非阻塞 I/O
    async with aiofiles.open("log.txt", "a") as f:
        await f.write(f"{request.method} {request.url.path}\n")

    return await call_next(request)

緩存重複計算

對於重複計算的結果,應該使用緩存來提高性能:

class CachingMiddleware(BaseHTTPMiddleware):
    def __init__(self, app):
        super().__init__(app)
        self.get_config = lru_cache(maxsize=100)(self._get_config)

    async def dispatch(self, request: Request, call_next):
        # 獲取配置(結果會被緩存)
        config = self.get_config(request.url.path)
        request.state.config = config
        return await call_next(request)

    def _get_config(self, path: str) -> dict:
        """昂貴的操作,但結果會被緩存"""
        # 模擬昂貴的計算或數據庫查詢
        time.sleep(0.1)
        # 返回路徑相關的配置
        return {"rate_limit": 100, "cache_ttl": 300}

延遲加載

對於不是每個請求都需要的資源,應該採用延遲加載策略:

class ExternalServiceMiddleware(BaseHTTPMiddleware):
    def __init__(self, app):
        super().__init__(app)
        self._session = None  # 不立即創建會話

    @property
    async def session(self):
        """延遲初始化 HTTP 會話"""
        if self._session is None:
            self._session = aiohttp.ClientSession()
        return self._session

    async def dispatch(self, request: Request, call_next):
        if request.url.path.startswith("/external/"):
            # 只有在需要時才獲取會話
            session = await self.session
            # 使用會話...

        return await call_next(request)

錯誤處理與穩健性

全面的錯誤處理

中間件應該處理所有可能發生的異常,確保應用的穩定性:

class RobustMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        try:
            # 中間件邏輯...
            response = await call_next(request)
            return response

        except HTTPException as e:
            # 處理 FastAPI 的 HTTP 異常
            logger.warning(f"HTTP Exception: {e.detail}")
            raise  # 重新拋出 HTTP 異常

        except Exception as e:
            # 處理未捕獲的異常
            logger.error(f"Unhandled exception: {str(e)}\n{traceback.format_exc()}")

            # 返回用戶友好的錯誤信息
            return JSONResponse(
                status_code=500,
                content={"detail": "An unexpected error occurred"}
            )

優雅降級

當某些功能不可用時,中間件應該能夠優雅降級:

class ExternalServiceMiddleware(BaseHTTPMiddleware):
    def __init__(self, app, service_url: str, timeout: float = 1.0):
        super().__init__(app)
        self.service_url = service_url
        self.timeout = timeout
        self.fallback_data = {"status": "offline", "data": []}  # 後備數據

    async def dispatch(self, request: Request, call_next):
        try:
            # 嘗試獲取外部服務數據,設置超時
            async with asyncio.timeout(self.timeout):
                # 調用外部服務...
                request.state.service_data = data
        except Exception as e:
            # 服務不可用,使用後備數據
            logger.warning(f"External service unavailable: {str(e)}")
            request.state.service_data = self.fallback_data

        # 繼續處理請求
        return await call_next(request)

重試機制

對於不可靠的操作,可以實現重試機制:

class RetryMiddleware(BaseHTTPMiddleware):
    def __init__(self, app, max_retries: int = 3, retry_delay: float = 0.1):
        super().__init__(app)
        self.max_retries = max_retries
        self.retry_delay = retry_delay

    async def retry_with_backoff(self, func, *args, **kwargs):
        """使用退避策略重試函數"""
        retries = 0
        while True:
            try:
                return await func(*args, **kwargs)
            except Exception as e:
                retries += 1
                if retries > self.max_retries:
                    raise

                # 計算延遲時間(指數退避)
                delay = self.retry_delay * (2 ** (retries - 1))
                await asyncio.sleep(delay)

    async def dispatch(self, request: Request, call_next):
        # 對於關鍵 API,使用重試機制
        if request.url.path.startswith("/api/critical/"):
            return await self.retry_with_backoff(call_next, request)
        else:
            return await call_next(request)

中間件組織與管理

中間件分組

對相關的中間件進行分組,便於管理和配置:

class MiddlewareGroup:
    def __init__(self, app: FastAPI, name: str):
        self.app = app
        self.name = name
        self.middleware_list = []

    def add(self, middleware_class, **options):
        """添加中間件到組"""
        self.middleware_list.append((middleware_class, options))
        return self

    def apply(self):
        """將所有中間件應用到應用"""
        # 反向應用,確保執行順序與添加順序一致
        for middleware_class, options in reversed(self.middleware_list):
            self.app.add_middleware(middleware_class, **options)
        return self.app

# 使用示例
security_middleware = MiddlewareGroup(app, "security")
security_middleware.add(
    CORSMiddleware,
    allow_origins=["https://example.com"]
).add(
    AuthMiddleware,
    secret_key="your-secret-key"
)

# 應用安全中間件組
security_middleware.apply()

條件中間件應用

根據環境或配置條件應用不同的中間件:

# 獲取環境配置
ENV = os.environ.get("APP_ENV", "development")
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"

# 根據環境應用不同的中間件
if ENV == "development":
    # 開發環境中間件
    app.add_middleware(
        LoggingMiddleware,
        log_level="debug",
        include_headers=True
    )
elif ENV == "production":
    # 生產環境中間件
    app.add_middleware(
        LoggingMiddleware,
        log_level="info"
    )

    app.add_middleware(
        RateLimitMiddleware,
        limit=100
    )

# 所有環境通用的中間件
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"] if ENV == "development" else ["https://example.com"]
)

監控與可觀測性

中間件性能指標

收集和報告中間件性能指標,幫助識別潛在問題:

class MetricsMiddleware(BaseHTTPMiddleware):
    def __init__(self, app):
        super().__init__(app)
        self.request_times = {}  # 儲存請求處理時間

    async def dispatch(self, request: Request, call_next):
        path = request.url.path
        method = request.method
        endpoint = f"{method} {path}"

        # 記錄開始時間
        start_time = time.time()

        # 處理請求
        response = await call_next(request)

        # 計算處理時間
        process_time = time.time() - start_time

        # 更新指標
        if endpoint not in self.request_times:
            self.request_times[endpoint] = []

        self.request_times[endpoint].append(process_time)

        # 限制列表大小
        if len(self.request_times[endpoint]) > 100:
            self.request_times[endpoint] = self.request_times[endpoint][-100:]

        # 每 100 個請求報告一次
        if len(self.request_times[endpoint]) % 100 == 0:
            avg_time = sum(self.request_times[endpoint]) / len(self.request_times[endpoint])
            print(f"Average processing time for {endpoint}: {avg_time:.4f}s")

        return response

請求跟蹤

添加請求跟蹤標識符,便於日誌關聯和問題排查:

class RequestTracingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # 生成唯一的請求 ID
        request_id = str(uuid.uuid4())

        # 將請求 ID 添加到請求狀態
        request.state.request_id = request_id

        # 記錄請求開始
        print(f"[{request_id}] Request started: {request.method} {request.url.path}")

        # 處理請求
        response = await call_next(request)

        # 將請求 ID 添加到響應頭
        response.headers["X-Request-ID"] = request_id

        # 記錄請求結束
        print(f"[{request_id}] Request completed: Status {response.status_code}")

        return response

常見陷阱與解決方案

中間件執行順序

FastAPI 中間件的執行順序與添加順序相反,後添加的先執行:

# 中間件執行順序:C -> B -> A
app.add_middleware(MiddlewareA)
app.add_middleware(MiddlewareB)
app.add_middleware(MiddlewareC)

# 如果需要確保特定順序,可以使用數字前綴命名
app.add_middleware(Middleware3_Last)
app.add_middleware(Middleware2_Middle)
app.add_middleware(Middleware1_First)

避免修改不可變對象

請求和響應的某些部分是不可變的,應該小心處理:

# 錯誤:嘗試直接修改請求的 URL
@app.middleware("http")
async def wrong_middleware(request: Request, call_next):
    # 這會失敗,因為 URL 是不可變的
    request.url.path = "/modified" + request.url.path
    return await call_next(request)

# 正確:使用請求狀態存儲額外信息
@app.middleware("http")
async def correct_middleware(request: Request, call_next):
    # 將原始路徑存儲在請求狀態中
    request.state.original_path = request.url.path
    return await call_next(request)

正確處理響應流

對於流式響應,中間件需要特殊處理:

class StreamMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        response = await call_next(request)

        # 檢查是否為流式響應
        if response.__class__.__name__ == "StreamingResponse":
            # 不要嘗試讀取或修改響應體
            return response

        # 處理普通響應
        # ...

        return response

關鍵要點

  • 遵循單一職責原則,每個中間件只處理一個關注點
  • 設計可配置、可測試的中間件
  • 避免阻塞操作,使用緩存和延遲加載提高性能
  • 實現全面的錯誤處理和優雅降級機制
  • 根據環境和需求組織和管理中間件
  • 添加監控和可觀測性功能
  • 注意中間件執行順序和不可變對象處理

通過合理應用這些最佳實踐,可以充分發揮 FastAPI 中間件的強大功能,構建高質量的 Web API。