الوسائط — اعتراض الطلبات والاستجابات وتحسينها
الوسائط مثل نقطة التفتيش الأمنية في المطار—كل راكب (طلب) يجب أن يمر عبر الجمارك والأمن وبوابة الصعود بالتسلسل، وفي أي من هذه المحطات قد يُسمح للراكب بالمتابعة أو إيقافه، بينما رحلة العودة (الاستجابة) تتبع الترتيب العكسي.
1. ما ستتعلمه
- كيف تعمل وسائط Starlette: سلسلة تغليف ASGI القابلة للاستدعاء
- الوسائط المدمجة: تكوين
CORSMiddlewareوسياسات الأمان - وسائط مخصصة: توقيت الطلبات، حقن معرف الطلب، حقن رأس الاستجابة
- ترتيب تنفيذ الوسائط: العلاقة بين ترتيب التسجيل وترتيب التنفيذ الفعلي
- سيناريو Alice: إضافة وسيطة تسجيل طلبات ووسيطة تحديد معدل API إلى PriceTracker
2. قصة Alice الحقيقية
(1) المشكلة: واجهة API تتعرض لارتفاعات حركة مرور خبيثة ولا يمكن تتبعها
بعد إطلاق Alice لواجهة PriceTracker API، اكتشفت أن عنوان IP معين كان يرسل 5000 طلب في الدقيقة، مما أدى إلى ارتفاع حمل قاعدة البيانات بشكل كبير. لزيادة الأمور سوءًا، عندما استدعى واجهة Bob الأمامية واجهة API من localhost:3000، تم حظر الطلبات من خلال سياسة CORS الخاصة بالمتصفح، مما أدى إلى فشل جميع الطلبات. احتاجت Alice إلى حل مشكلتي الوصول عبر الأصل وتحديد معدل الطلبات في وقت واحد، لكن Flask يفتقر إلى آلية وسائط موحدة.
(2) حلول وسائط FastAPI
توفر FastAPI/Starlette وسائط بنمط البصل، حيث تعالج كل طبقة مشكلة محددة: وسيطة CORS تعالج الطلبات عبر الأصل، ووسيطة تحديد المعدل تتحكم في تكرار الطلبات، ووسيطة التسجيل تسجل الطلبات—لا تتداخل مع بعضها البعض وتصبح سارية فور التسجيل.
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["http://localhost:3000"])
(3) العائد
حل Bob مشكلة الوصول عبر الأصل بواجهة الأمامية بخمسة أسطر فقط من التعليمات البرمجية. قللت وسيطة تحديد المعدل الطلبات من عناوين IP الخبيثة من 5000 إلى 1000 طلب في الدقيقة، وخصصت وسيطة التسجيل معرفًا فريدًا لكل طلب لسهولة التتبع.
3. نموذج البصل للوسائط
(1) مبدأ نموذج البصل
flowchart TD
Request[Client Request] --> CORS[CORS Middleware]
CORS --> Logging[Logging Middleware]
Logging --> RateLimit[Rate Limit Middleware]
RateLimit --> App[FastAPI Application]
App --> RateLimit2[Rate Limit Response]
RateLimit2 --> Logging2[Logging Response]
Logging2 --> CORS2[CORS Response]
CORS2 --> Response[Client Response]
RateLimit -.->|429 Too Many Requests| Reject[Short-circuit Rejection]
Reject --> Logging2
| الميزة | الوصف |
|---|---|
| اتجاه الطلب | من الخارج إلى الداخل (الطبقة الخارجية المسجلة أولاً) |
| اتجاه الاستجابة | من الداخل إلى الخارج (عكس اتجاه الطلب) |
| قدرة الدائرة القصيرة | أي وسيطة يمكنها إرجاع استجابة مبكرًا، دون الانتقال إلى الطبقة التالية |
| ترتيب التسجيل | آخر وسيطة مسجلة هي أول من يعالج الطلب |
(1) ▶مثال: ترتيب تسجيل الوسائط وترتيب التنفيذ
from fastapi import FastAPI, Request
import time
app = FastAPI()
# الوسيطة المسجلة أولاً = الطبقة الخارجية (تنفذ أولاً عند الطلب)
@app.middleware("http")
async def outer_middleware(request: Request, call_next):
print("Outer: before request")
response = await call_next(request)
print("Outer: after response")
return response
# الوسيطة المسجلة أخيرًا = الطبقة الداخلية (تنفذ أخيرًا عند الطلب)
@app.middleware("http")
async def inner_middleware(request: Request, call_next):
print("Inner: before request")
response = await call_next(request)
print("Inner: after response")
return response
@app.get("/test")
async def test():
print("Handler: processing")
return {"ok": True}
الناتج:
Outer: before request
Inner: before request
Handler: processing
Inner: after response
Outer: after response
4. وسيطة CORS
(1) تكوين مشاركة الموارد عبر الأصل
CORS (مشاركة الموارد عبر الأصل) هي سياسة أمان المتصفح التي تقيّد صفحات الويب من أصول مختلفة من الوصول إلى واجهات API. عندما تصل واجهة Bob الأمامية (localhost:3000) إلى واجهة Alice API (localhost:8000)، يشكل ذلك طلبًا عبر الأصل.
(1) ▶مثال: تكوين CORS لبيئة التطوير
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # واجهة Bob الأمامية
allow_credentials=True,
allow_methods=["*"], # جميع طرق HTTP
allow_headers=["*"], # جميع الرؤوس
)
الناتج:
# تم التنفيذ بنجاح
(2) ▶مثال: تكوين CORS لبيئة الإنتاج
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
app = FastAPI()
ALLOWED_ORIGINS = [
"https://pricetracker.example.com",
"https://admin.pricetracker.example.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
الناتج:
# تم التنفيذ بنجاح
| إعدادات CORS | بيئة التطوير | بيئة الإنتاج |
|---|---|---|
allow_origins |
["*"] أو localhost |
قائمة بأسماء النطاقات المحددة |
allow_credentials |
True | True (إذا كانت ملفات تعريف الارتباط مطلوبة) |
allow_methods |
["*"] |
الطرق المطلوبة فقط |
allow_headers |
["*"] |
الرؤوس المطلوبة فقط |
max_age |
الافتراضي | 3600 (تخزين الفحص المسبق المؤقت) |
5. وسائط مخصصة
(1) وسيطة توقيت الطلبات
(1) ▶مثال: تسجيل وقت المعالجة لكل طلب
from fastapi import FastAPI, Request
import time
app = FastAPI()
@app.middleware("http")
async def timing_middleware(request: Request, call_next):
start_time = time.perf_counter()
response = await call_next(request)
process_time = time.perf_counter() - start_time
response.headers["X-Process-Time"] = f"{process_time:.4f}s"
return response
@app.get("/products")
async def list_products():
return [{"id": 1, "name": "Widget"}]
الناتج:
# تم تعريف الدالة بنجاح
(2) ▶مثال: وسيطة حقن معرف الطلب
import uuid
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def request_id_middleware(request: Request, call_next):
request_id = str(uuid.uuid4())
request.state.request_id = request_id # إرفاق بحالة الطلب
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
@app.get("/health")
async def health_check(request: Request):
return {
"status": "healthy",
"request_id": request.state.request_id,
}
الناتج:
# تم تعريف الدالة بنجاح
(3) ▶مثال: وسيطة تحديد معدل API (نسخة مبسطة)
from fastapi import FastAPI, Request, HTTPException
from collections import defaultdict
import time
app = FastAPI()
# محدد معدل بسيط في الذاكرة
rate_limits: dict[str, list[float]] = defaultdict(list)
RATE_LIMIT = 1000 # طلبات في الدقيقة
WINDOW = 60 # ثوانٍ
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
client_ip = request.client.host if request.client else "unknown"
now = time.time()
# تنظيف الإدخالات القديمة
rate_limits[client_ip] = [
t for t in rate_limits[client_ip] if now - t < WINDOW
]
# التحقق من الحد
if len(rate_limits[client_ip]) >= RATE_LIMIT:
raise HTTPException(
status_code=429,
detail=f"Rate limit exceeded: {RATE_LIMIT} requests per {WINDOW}s",
)
rate_limits[client_ip].append(now)
response = await call_next(request)
response.headers["X-RateLimit-Limit"] = str(RATE_LIMIT)
response.headers["X-RateLimit-Remaining"] = str(
RATE_LIMIT - len(rate_limits[client_ip])
)
return response
الناتج:
# تم تعريف الدالة بنجاح
6. وسائط قائمة على الأصناف واستدعاءات ASGI
(1) كيفية كتابة وسائط قائمة على الأصناف
لمنطق الوسائط الأكثر تعقيدًا، نوصي باستخدام نهج قائم على الأصناف يتلاعب مباشرة بنطاق ASGI واستقباله وإرساله.
(1) ▶مثال: وسيطة تسجيل طلبات قائمة على الأصناف
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
import logging
import time
logger = logging.getLogger("pricetracker")
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response:
start = time.perf_counter()
logger.info(f"Request: {request.method} {request.url.path}")
response = await call_next(request)
duration = time.perf_counter() - start
logger.info(
f"Response: {request.method} {request.url.path} "
f"status={response.status_code} duration={duration:.4f}s"
)
return response
# تسجيل وسيطة قائمة على الأصناف
app = FastAPI()
app.add_middleware(LoggingMiddleware)
الناتج:
# تم تعريف الدالة بنجاح
(2) مقارنة أنواع الوسائط
| النهج | السيناريوهات القابلة للتطبيق | المزايا | العيوب |
|---|---|---|---|
@app.middleware("http") |
اعتراض بسيط | أقل قدر من التعليمات البرمجية | يعالج HTTP فقط |
BaseHTTPMiddleware |
تعقيد متوسط | قابل للتكوين | يعالج HTTP فقط |
| صنف ASGI خالص | تحكم كامل | يعالج أيضًا WebSockets | تعليمات برمجية معقدة |
❓أسئلة شائعة
@app.middleware تأتي قبل add_middleware (على مستوى أعلى). يُوصى باستخدام نهج متسق.allow_origins في CORS؟allow_credentials=True عند تحديد *؛ سيرفضه المتصفح.BaseHTTPMiddleware (استنزاف الدفق بعد قراءة المحتوى)؛ للسيناريوهات المعقدة، نوصي باستخدام وسيطة ASGI خالصة.if settings.ENABLE_RATE_LIMIT: app.add_middleware(RateLimitMiddleware).📖ملخص
- تتبع الوسائط نموذج البصل: الطلبات تنتقل من الخارج إلى الداخل، والاستجابات تنتقل من الداخل إلى الخارج؛ أي طبقة يمكنها إرجاع استجابة مبكرًا
CORSMiddlewareلحل مشاكل الوصول عبر الأصل، يجب تحديد اسم نطاق محدد في بيئة الإنتاج- الوسائط المخصصة يمكنها تنفيذ اهتمامات شاملة مثل توقيت الطلبات وحقن معرف الطلب وتحديد المعدل
- ترتيب تسجيل الوسائط يحدد ترتيب التنفيذ: آخر وسيطة مسجلة هي أول من يعالج الطلبات (الطبقة الداخلية)
- الوسائط القائمة على الأصناف (
BaseHTTPMiddleware) مناسبة للمنطق المعقد؛ أصناف ASGI الخالصة يمكنها التعامل مع WebSockets
📝تمارين
- مشكلة أساسية (الصعوبة ⭐): أضف وسيطة CORS إلى PriceTracker للسماح بالوصول عبر الأصل من
http://localhost:3000، وتحقق في المتصفح أن واجهة Bob الأمامية يمكنها استدعاء API. تلميح:app.add_middleware(CORSMiddleware, ...) - تمرين متقدم (الصعوبة ⭐⭐): نفّذ وسيطة توقيت طلبات تضيف
X-Process-Timeإلى رؤوس الاستجابة، واستخدم Swagger UI لعرض رؤوس الاستجابة. تلميح:@app.middleware("http")+time.perf_counter() - تحدي (الصعوبة ⭐⭐⭐): نفّذ وسيطة تحديد معدل بناءً على IP (1000 طلب في الدقيقة). عند تجاوز الحد، أرجع رمز الحالة 429 ورأس الاستجابة
Retry-After، مع عرض الحصة المتبقية في رأس الاستجابة. تلميح:defaultdict(list)+ تنظيف نافذة الزمن
---|



