""" 回應壓縮中間件 使用 gzip 壓縮 API 回應,減少傳輸大小 """ import gzip from typing import Callable from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import Response from core.logging import get_logger logger = get_logger("middleware.compression") # 最小壓縮大小(bytes) MIN_COMPRESS_SIZE = 500 # 可壓縮的 Content-Type COMPRESSIBLE_TYPES = { "application/json", "text/html", "text/plain", "text/css", "text/javascript", "application/javascript", } class GzipMiddleware(BaseHTTPMiddleware): """ Gzip 壓縮中間件 功能: 1. 檢查客戶端是否支援 gzip 2. 壓縮大於閾值的回應 3. 只壓縮可壓縮的 Content-Type """ async def dispatch(self, request: Request, call_next: Callable) -> Response: # 檢查客戶端是否支援 gzip accept_encoding = request.headers.get("accept-encoding", "") supports_gzip = "gzip" in accept_encoding.lower() response = await call_next(request) # 不支援 gzip 或已經壓縮,直接返回 if not supports_gzip: return response if response.headers.get("content-encoding"): return response # 檢查 Content-Type 是否可壓縮 content_type = response.headers.get("content-type", "") base_type = content_type.split(";")[0].strip() if base_type not in COMPRESSIBLE_TYPES: return response # 讀取回應內容 body = b"" async for chunk in response.body_iterator: body += chunk # 檢查大小是否值得壓縮 if len(body) < MIN_COMPRESS_SIZE: # 重建回應 return Response( content=body, status_code=response.status_code, headers=dict(response.headers), media_type=response.media_type, ) # 壓縮內容 compressed = gzip.compress(body, compresslevel=6) # 只有壓縮後更小才使用 if len(compressed) >= len(body): return Response( content=body, status_code=response.status_code, headers=dict(response.headers), media_type=response.media_type, ) # 更新標頭 headers = dict(response.headers) headers["content-encoding"] = "gzip" headers["content-length"] = str(len(compressed)) # 移除可能衝突的標頭 headers.pop("transfer-encoding", None) logger.debug( f"壓縮回應: {len(body)} -> {len(compressed)} bytes " f"({100 - len(compressed) * 100 // len(body)}% 減少)" ) return Response( content=compressed, status_code=response.status_code, headers=headers, media_type=response.media_type, )