با میانافزار FastAPI از پایه مسلط شوید. این راهنمای عمیق میانافزارهای سفارشی، احراز هویت، ثبت وقایع، مدیریت خطا و بهترین روشها برای ساخت APIهای قدرتمند را پوشش میدهد.
میانافزار (Middleware) در FastAPI پایتون: راهنمای جامع پردازش درخواستها و پاسخها
در دنیای توسعه وب مدرن، عملکرد، امنیت و قابلیت نگهداری از اهمیت بالایی برخوردارند. فریمورک FastAPI پایتون به دلیل سرعت فوقالعاده و ویژگیهای کاربرپسند خود به سرعت محبوبیت پیدا کرده است. یکی از قدرتمندترین و در عین حال گاهی اوقات سوءتفاهم شدهترین ویژگیهای آن، میانافزار (middleware) است. میانافزار به عنوان یک حلقه حیاتی در زنجیره پردازش درخواست و پاسخ عمل میکند و به توسعهدهندگان اجازه میدهد تا کدها را اجرا کرده، دادهها را تغییر داده و قوانین را قبل از رسیدن درخواست به مقصد یا قبل از ارسال پاسخ به مشتری اعمال کنند.
این راهنمای جامع برای مخاطبان جهانی توسعهدهندگان، از کسانی که تازه با FastAPI شروع کردهاند تا متخصصان باتجربه که به دنبال تعمیق درک خود هستند، طراحی شده است. ما مفاهیم اصلی میانافزار را بررسی خواهیم کرد، نحوه ساخت راهحلهای سفارشی را نشان خواهیم داد و موارد استفاده عملی و واقعی را گام به گام توضیح میدهیم. در پایان، شما مجهز خواهید بود تا از میانافزار برای ساخت APIهای قویتر، ایمنتر و کارآمدتر استفاده کنید.
میانافزار (Middleware) در بستر فریمورکهای وب چیست؟
قبل از ورود به کدنویسی، درک مفهوم ضروری است. چرخه درخواست-پاسخ برنامه خود را به عنوان یک خط لوله یا یک خط مونتاژ تصور کنید. هنگامی که یک مشتری درخواستی را به API شما ارسال میکند، بلافاصله به منطق نقطه پایانی شما نمیرسد. در عوض، از طریق یک سری مراحل پردازش عبور میکند. به همین ترتیب، هنگامی که نقطه پایانی شما پاسخی تولید میکند، قبل از رسیدن به مشتری، از طریق همین مراحل باز میگردد. اجزای میانافزار دقیقاً همین مراحل در خط لوله هستند.
یک قیاس محبوب، مدل پیاز است. هسته پیاز، منطق کسبوکار برنامه شما (نقطه پایانی) است. هر لایه از پیاز که هسته را احاطه کرده است، یک قطعه میانافزار است. یک درخواست باید از هر لایه بیرونی عبور کند تا به هسته برسد و پاسخ نیز از طریق همان لایهها به بیرون باز میگردد. هر لایه میتواند درخواست را در مسیر ورودی و پاسخ را در مسیر خروجی بازرسی و اصلاح کند.
در اصل، میانافزار یک تابع یا کلاس است که به شیء درخواست، شیء پاسخ و میانافزار بعدی در چرخه درخواست-پاسخ برنامه دسترسی دارد. اهداف اصلی آن شامل موارد زیر است:
- اجرای کد: انجام اقداماتی برای هر درخواست ورودی، مانند ثبت وقایع یا نظارت بر عملکرد.
- تغییر درخواست و پاسخ: اضافه کردن هدرها، فشردهسازی بدنه پاسخ یا تبدیل فرمتهای داده.
- کوتاه کردن چرخه (Short-circuiting): پایان دادن به چرخه درخواست-پاسخ زودتر از موعد. به عنوان مثال، یک میانافزار احراز هویت میتواند یک درخواست احراز هویت نشده را قبل از اینکه به نقطه پایانی مورد نظر برسد، مسدود کند.
- مدیریت مسائل عمومی: رسیدگی به مسائل چندوجهی (cross-cutting concerns) مانند مدیریت خطا، CORS (به اشتراکگذاری منابع بین مبدأها) و مدیریت نشست (session management) در یک مکان متمرکز.
FastAPI بر پایه ابزار Starlette ساخته شده است که یک پیادهسازی قوی از استاندارد ASGI (رابط دروازه سرور ناهمزمان) را فراهم میکند. میانافزار یک مفهوم اساسی در ASGI است و آن را به یک مؤلفه اصلی در اکوسیستم FastAPI تبدیل میکند.
سادهترین شکل: میانافزار FastAPI با یک دکوراتور
FastAPI یک روش ساده برای افزودن میانافزار با استفاده از دکوراتور @app.middleware("http") فراهم میکند. این روش برای منطق ساده و خودکفا که نیاز به اجرا برای هر درخواست HTTP دارد، عالی است.
بیایید یک مثال کلاسیک ایجاد کنیم: یک میانافزار برای محاسبه زمان پردازش هر درخواست و اضافه کردن آن به هدرهای پاسخ. این برای نظارت بر عملکرد فوقالعاده مفید است.
مثال: میانافزار زمان پردازش
ابتدا، مطمئن شوید که FastAPI و یک سرور ASGI مانند Uvicorn را نصب کردهاید:
pip install fastapi uvicorn
حالا، بیایید کد را در فایلی به نام main.py بنویسیم:
import time
from fastapi import FastAPI, Request
app = FastAPI()
# Define the middleware function
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
# Record the start time when the request comes in
start_time = time.time()
# Proceed to the next middleware or the endpoint
response = await call_next(request)
# Calculate the processing time
process_time = time.time() - start_time
# Add the custom header to the response
response.headers["X-Process-Time"] = str(process_time)
return response
@app.get("/")
async def root():
# Simulate some work
time.sleep(0.5)
return {"message": "Hello, World!"}
برای اجرای این برنامه، از دستور زیر استفاده کنید:
uvicorn main:app --reload
حالا، اگر با استفاده از ابزاری مانند cURL یا یک کلاینت API مانند Postman، درخواستی به http://127.0.0.1:8000 ارسال کنید، یک هدر جدید در پاسخ، X-Process-Time، با مقداری حدود 0.5 ثانیه مشاهده خواهید کرد.
بررسی کد:
@app.middleware("http"): این دکوراتور تابع ما را به عنوان یک قطعه میانافزار HTTP ثبت میکند.async def add_process_time_header(request: Request, call_next):: تابع میانافزار باید ناهمزمان باشد. این تابع شیءRequestورودی و یک تابع خاص،call_nextرا دریافت میکند.response = await call_next(request): این مهمترین خط است.call_nextدرخواست را به مرحله بعدی در خط لوله (یا یک میانافزار دیگر یا عملیات مسیر واقعی) منتقل میکند. شما باید این فراخوانی را `await` کنید. نتیجه، شیءResponseتولید شده توسط نقطه پایانی است.response.headers[...] = ...: پس از دریافت پاسخ از نقطه پایانی، میتوانیم آن را تغییر دهیم، در این مورد با افزودن یک هدر سفارشی.return response: در نهایت، پاسخ اصلاح شده برای ارسال به مشتری بازگردانده میشود.
ساخت میانافزار سفارشی خود با کلاسها
در حالی که رویکرد دکوراتور ساده است، برای سناریوهای پیچیدهتر، به ویژه زمانی که میانافزار شما نیاز به پیکربندی یا مدیریت وضعیت داخلی دارد، میتواند محدودکننده باشد. برای این موارد، FastAPI (از طریق Starlette) از میانافزار مبتنی بر کلاس با استفاده از BaseHTTPMiddleware پشتیبانی میکند.
یک رویکرد مبتنی بر کلاس ساختار بهتری را ارائه میدهد، امکان تزریق وابستگی (dependency injection) در سازنده خود را فراهم میکند و به طور کلی برای منطق پیچیده قابلیت نگهداری بیشتری دارد. منطق اصلی در یک متد ناهمزمان dispatch قرار میگیرد.
مثال: میانافزار احراز هویت با کلید API مبتنی بر کلاس
بیایید یک میانافزار عملیتر بسازیم که API ما را ایمن میکند. این میانافزار یک هدر خاص، X-API-Key را بررسی میکند و اگر کلید وجود نداشته باشد یا نامعتبر باشد، بلافاصله یک پاسخ خطای 403 Forbidden را برمیگرداند. این نمونهای از "کوتاه کردن" درخواست است.
در main.py:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.responses import Response
# A list of valid API keys. In a real application, this would come from a database or a secure vault.
VALID_API_KEYS = ["my-super-secret-key", "another-valid-key"]
class APIKeyMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
api_key = request.headers.get("X-API-Key")
if api_key not in VALID_API_KEYS:
# Short-circuit the request and return an error response
return JSONResponse(
status_code=403,
content={"detail": "Forbidden: Invalid or missing API Key"}
)
# If the key is valid, proceed with the request
response = await call_next(request)
return response
app = FastAPI()
# Add the middleware to the application
app.add_middleware(APIKeyMiddleware)
@app.get("/")
async def root():
return {"message": "Welcome to the secure zone!"}
حالا، هنگامی که این برنامه را اجرا میکنید:
- درخواستی بدون هدر
X-API-Key(یا با یک مقدار اشتباه) کد وضعیت 403 و پیام خطای JSON را دریافت خواهد کرد. - درخواستی با هدر
X-API-Key: my-super-secret-keyموفق خواهد بود و پاسخ 200 OK را دریافت میکند.
این الگو بسیار قدرتمند است. کد نقطه پایانی در / نیازی به دانستن چیزی در مورد اعتبار سنجی کلید API ندارد؛ این نگرانی به طور کامل به لایه میانافزار منتقل شده است.
موارد استفاده رایج و قدرتمند برای میانافزار
میانافزار ابزاری عالی برای رسیدگی به مسائل چندوجهی (cross-cutting concerns) است. بیایید برخی از رایجترین و تأثیرگذارترین موارد استفاده را بررسی کنیم.
1. ثبت وقایع متمرکز (Centralized Logging)
ثبت وقایع جامع برای برنامههای تولیدی غیرقابل چشمپوشی است. میانافزار به شما امکان میدهد یک نقطه واحد ایجاد کنید که در آن اطلاعات حیاتی مربوط به هر درخواست و پاسخ مربوطه را ثبت کنید.
مثال میانافزار ثبت وقایع:
import logging
from fastapi import FastAPI, Request
import time
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI()
@app.middleware("http")
async def logging_middleware(request: Request, call_next):
start_time = time.time()
# Log request details
logger.info(f"Incoming request: {request.method} {request.url.path}")
response = await call_next(request)
process_time = time.time() - start_time
# Log response details
logger.info(f"Response status: {response.status_code} | Process time: {process_time:.4f}s")
return response
این میانافزار متد و مسیر درخواست را در هنگام ورود و کد وضعیت پاسخ و زمان کل پردازش را در هنگام خروج ثبت میکند. این امر دید بینهایت ارزشمندی را در مورد ترافیک برنامه شما فراهم میکند.
2. مدیریت خطای جهانی (Global Error Handling)
به طور پیشفرض، یک استثنای مدیریت نشده در کد شما منجر به خطای داخلی سرور 500 میشود که به طور بالقوه ردیابیهای پشته و جزئیات پیادهسازی را به مشتری نشان میدهد. یک میانافزار مدیریت خطای جهانی میتواند همه استثناها را دریافت کند، آنها را برای بررسی داخلی ثبت کند و یک پاسخ خطای استاندارد و کاربرپسند را برگرداند.
مثال میانافزار مدیریت خطا:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import logging
logger = logging.getLogger(__name__)
app = FastAPI()
@app.middleware("http")
async def error_handling_middleware(request: Request, call_next):
try:
return await call_next(request)
except Exception as e:
logger.error(f"An unhandled error occurred: {e}", exc_info=True)
return JSONResponse(
status_code=500,
content={"detail": "An internal server error occurred. Please try again later."}
)
@app.get("/error")
async def cause_error():
return 1 / 0 # This will raise a ZeroDivisionError
با وجود این میانافزار، درخواست به /error دیگر باعث از کار افتادن سرور یا نمایش ردیابی پشته نخواهد شد. در عوض، با ظرافت یک کد وضعیت 500 با یک بدنه JSON تمیز را برمیگرداند، در حالی که خطای کامل در سمت سرور برای بررسی توسعهدهندگان ثبت میشود.
3. CORS (به اشتراکگذاری منابع بین مبدأها)
اگر برنامه فرانتاند شما از یک دامنه، پروتکل یا پورت متفاوت از بکاند FastAPI شما سرویس داده میشود، مرورگرها درخواستها را به دلیل سیاست Same-Origin مسدود خواهند کرد. CORS مکانیزمی برای کاهش این سیاست است. FastAPI یک CORSMiddleware اختصاصی و بسیار قابل تنظیم برای این منظور فراهم میکند.
مثال پیکربندی CORS:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Define the list of allowed origins. Use "*" for public APIs, but be specific for better security.
origins = [
"http://localhost:3000",
"https://my-production-frontend.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True, # Allow cookies to be included in cross-origin requests
allow_methods=["*"], # Allow all standard HTTP methods
allow_headers=["*"], # Allow all headers
)
این یکی از اولین قطعات میانافزاری است که احتمالاً به هر پروژه با فرانتاند جداگانه اضافه میکنید و مدیریت سیاستهای بین مبدأ را از یک مکان واحد و مرکزی ساده میکند.
4. فشردهسازی GZip
فشردهسازی پاسخهای HTTP میتواند اندازه آنها را به طور قابل توجهی کاهش دهد و منجر به زمان بارگذاری سریعتر برای مشتریان و هزینههای پهنای باند کمتر شود. FastAPI شامل GZipMiddleware برای رسیدگی خودکار به این موضوع است.
مثال میانافزار GZip:
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware
app = FastAPI()
# Add the GZip middleware. You can set a minimum size for compression.
app.add_middleware(GZipMiddleware, minimum_size=1000)
@app.get("/")
async def root():
# This response is small and will not be gzipped.
return {"message": "Hello World"}
@app.get("/large-data")
async def large_data():
# This large response will be automatically gzipped by the middleware.
return {"data": "a_very_long_string..." * 1000}
با این میانافزار، هر پاسخی که بزرگتر از 1000 بایت باشد، در صورت تایید مشتری (که تقریباً همه مرورگرها و کلاینتهای مدرن انجام میدهند) فشردهسازی میشود.
مفاهیم پیشرفته و بهترین روشها
همانطور که در استفاده از میانافزار مهارت بیشتری پیدا میکنید، درک برخی نکات ظریف و بهترین روشها برای نوشتن کدهای تمیز، کارآمد و قابل پیشبینی اهمیت دارد.
1. ترتیب میانافزار اهمیت دارد!
این مهمترین قانونی است که باید به خاطر بسپارید. میانافزار به ترتیبی که به برنامه اضافه میشود، پردازش میگردد. اولین میانافزار اضافه شده، بیرونیترین لایه "پیاز" است.
این تنظیمات را در نظر بگیرید:
app.add_middleware(ErrorHandlingMiddleware) # بیرونیترین لایه
app.add_middleware(LoggingMiddleware)
app.add_middleware(AuthenticationMiddleware) # درونیترین لایه
جریان یک درخواست به این صورت خواهد بود:
ErrorHandlingMiddlewareدرخواست را دریافت میکند. این میانافزارcall_nextخود را در یک بلوکtry...exceptقرار میدهد.- این میانافزار
nextرا فراخوانی کرده و درخواست را بهLoggingMiddlewareارسال میکند. LoggingMiddlewareدرخواست را دریافت کرده، آن را ثبت کرده وnextرا فراخوانی میکند.AuthenticationMiddlewareدرخواست را دریافت کرده، اعتبارنامهها را تأیید میکند وnextرا فراخوانی میکند.- درخواست در نهایت به نقطه پایانی میرسد.
- نقطه پایانی پاسخی را برمیگرداند.
AuthenticationMiddlewareپاسخ را دریافت کرده و آن را به بالا منتقل میکند.LoggingMiddlewareپاسخ را دریافت کرده، آن را ثبت کرده و آن را به بالا منتقل میکند.ErrorHandlingMiddlewareپاسخ نهایی را دریافت کرده و آن را به مشتری برمیگرداند.
این ترتیب منطقی است: کنترلکننده خطا در بیرون قرار دارد تا بتواند خطاهای هر لایه بعدی، از جمله سایر میانافزارها را دریافت کند. لایه احراز هویت در عمق قرار دارد، بنابراین ما زحمت ثبت یا پردازش درخواستهایی را که به هر حال رد خواهند شد، به خود نمیدهیم.
2. انتقال داده با request.state
گاهی اوقات، یک میانافزار نیاز دارد اطلاعاتی را به نقطه پایانی منتقل کند. به عنوان مثال، یک میانافزار احراز هویت ممکن است یک JWT را رمزگشایی کرده و ID کاربر را استخراج کند. چگونه میتواند این ID کاربر را برای تابع عملیات مسیر در دسترس قرار دهد؟
روش اشتباه، تغییر مستقیم شیء درخواست است. روش صحیح، استفاده از شیء request.state است. این یک شیء ساده و خالی است که دقیقاً برای این منظور فراهم شده است.
مثال: انتقال دادههای کاربر از میانافزار
# در متد dispatch میانافزار احراز هویت شما:
# ... پس از اعتبارسنجی توکن و رمزگشایی کاربر ...
user_data = {"id": 123, "username": "global_dev"}
request.state.user = user_data
response = await call_next(request)
# در نقطه پایانی شما:
@app.get("/profile")
async def get_user_profile(request: Request):
current_user = request.state.user
return {"profile_for": current_user}
این کار منطق را تمیز نگه میدارد و از آلوده کردن فضای نام شیء Request جلوگیری میکند.
3. ملاحظات عملکرد
در حالی که میانافزار قدرتمند است، هر لایه مقدار کمی سربار اضافه میکند. برای برنامههای با عملکرد بالا، این نکات را در نظر داشته باشید:
- سبک نگه داشتن: منطق میانافزار باید تا حد امکان سریع و کارآمد باشد.
- ناهمزمان بودن: اگر میانافزار شما نیاز به انجام عملیات I/O (مانند بررسی پایگاه داده) دارد، اطمینان حاصل کنید که کاملاً
asyncاست تا از مسدود کردن حلقه رویداد سرور جلوگیری شود. - هدفمند استفاده کنید: میانافزارهایی را که نیاز ندارید اضافه نکنید. هر کدام به عمق پشته فراخوانی و زمان پردازش اضافه میکنند.
4. تست میانافزار شما
میانافزار بخش مهمی از منطق برنامه شماست و باید به طور کامل آزمایش شود. TestClient از FastAPI این کار را ساده میکند. میتوانید تستهایی بنویسید که درخواستها را با و بدون شرایط مورد نیاز (مثلاً با و بدون یک کلید API معتبر) ارسال کنند و تأیید کنند که میانافزار طبق انتظار عمل میکند.
مثال تست برای APIKeyMiddleware:
from fastapi.testclient import TestClient
from .main import app # برنامه FastAPI خود را وارد کنید
client = TestClient(app)
def test_request_without_api_key_is_forbidden():
response = client.get("/")
assert response.status_code == 403
assert response.json() == {"detail": "Forbidden: Invalid or missing API Key"}
def test_request_with_valid_api_key_is_successful():
headers = {"X-API-Key": "my-super-secret-key"}
response = client.get("/", headers=headers)
assert response.status_code == 200
assert response.json() == {"message": "Welcome to the secure zone!"}
نتیجهگیری
میانافزار FastAPI یک ابزار اساسی و قدرتمند برای هر توسعهدهندهای است که APIهای وب مدرن میسازد. این ابزار راهی ظریف و قابل استفاده مجدد برای رسیدگی به مسائل چندوجهی فراهم میکند و آنها را از منطق اصلی کسبوکار شما جدا میسازد. با رهگیری و پردازش هر درخواست و پاسخ، میانافزار به شما امکان میدهد ثبت وقایع قوی، مدیریت خطای متمرکز، سیاستهای امنیتی سختگیرانه و بهبودهای عملکردی مانند فشردهسازی را پیادهسازی کنید.
از دکوراتور ساده @app.middleware("http") گرفته تا راهحلهای پیچیده و مبتنی بر کلاس، انعطافپذیری لازم برای انتخاب رویکرد صحیح برای نیازهای خود را دارید. با درک مفاهیم اصلی، موارد استفاده رایج و بهترین روشهایی مانند ترتیب میانافزار و مدیریت وضعیت، میتوانید برنامههای FastAPI تمیزتر، ایمنتر و با قابلیت نگهداری بالا بسازید.
حالا نوبت شماست. شروع به ادغام میانافزار سفارشی در پروژه FastAPI بعدی خود کنید و سطح جدیدی از کنترل و ظرافت را در طراحی API خود باز کنید. امکانات بیشمار است و تسلط بر این ویژگی بدون شک شما را به یک توسعهدهنده مؤثرتر و کارآمدتر تبدیل خواهد کرد.