Middleware — קוד שרץ על כל בקשה
- להסביר מה זה Middleware ואיזו בעיה הוא פותר
- לכתוב Middleware עם @app.middleware(“http”) ו-call_next
- להבין מה עושים async ו-await בתוך Middleware
- להוסיף ל-API הסטודנטים לוג ומדידת זמן לכל בקשה, בלי לשנות אף Route
מה נלמד?
- איזו בעיה יש כשרוצים לעשות משהו בכל בקשה.
- מה זה Middleware, ואיך הוא קשור ל-Decorator שכבר מכירים.
@app.middleware("http"),call_next,asyncו-await.- לוג ומדידת זמן לכל בקשה ב-API הסטודנטים, ו-Header שמתווסף לכל תשובה.
למה צריך את זה?
ה-API הסטודנטים מהשיעור הקודם עובד. עכשיו רוצים לדעת מה קורה בו: איזו בקשה הגיעה, איזה קוד חזר, וכמה זמן היא לקחה. הדרך הראשונה שעולה לראש — להוסיף שורת לוג לכל Route:
@app.get("/students")
def list_students():
logging.info("GET /students")
return students
@app.get("/students/{student_id}")
def get_student(student_id: int):
logging.info("GET /students/" + str(student_id))
...שלוש בעיות:
- אותה שורה מועתקת לכל Route — חמש, עשר, חמישים פעמים.
- Route חדש שבו שכחו את השורה — פשוט לא נרשם.
- אין דרך לדעת כך את קוד התשובה — 404 ו-400 נוצרים ב-
raise HTTPException, אחרי שורת הלוג, ו-422 נוצר עוד לפני שה-Route רץ. וגם לא כמה זמן הבקשה לקחה.
הסבר מהבסיס
הרעיון — Middleware
Middleware (תוכנת ביניים) = קוד שעוטף כל בקשה שמגיעה לשרת: הוא רץ לפני ה-Route, מעביר את הבקשה הלאה, ורץ שוב אחרי — כשהתשובה כבר ביד.
Client --request--> Middleware --------> Route (your function)
before: start the timer
Client <-response-- Middleware <-------- Route
after: status code, duration, logכמו Decorator — לכל השרת
בשיעור על דקורטורים כתבנו Decorator שמודד זמן: הוא עוטף פונקציה אחת, רץ לפניה ואחריה, וקורא לה באמצע. Middleware הוא בדיוק אותו רעיון — רק שהוא עוטף את כל הבקשות לשרת, בלי לגעת באף Route.
| Decorator | Middleware | |
|---|---|---|
| עוטף | פונקציה אחת | כל בקשה לשרת |
| הקריאה באמצע | func(*args, **kwargs) | await call_next(request) |
| איפה מוסיפים | @timed מעל כל פונקציה | פעם אחת, ב-main.py |
כל מי שנכנס לבניין עובר ליד השומר — לא משנה לאיזו קומה הוא הולך. השומר רושם מי נכנס ומתי, ומי יצא ומתי. הוא לא צריך לעמוד ליד כל משרד. Middleware = השומר בכניסה, וה-Routes = המשרדים.
למה צריך לחכות נכון?
ה-Middleware מעביר את הבקשה ל-Route, ואז מחכה לתשובה. אבל שרת מקבל הרבה בקשות בבת אחת. אם הוא “נתקע” ומחכה לכל בקשה עד הסוף, כל השאר עומדות בתור.
מלצר מעביר הזמנה למטבח. אם הוא עומד ליד המטבח עד שהמנה מוכנה — כל השולחנות האחרים מחכים לו. מלצר טוב אומר “תודיעו לי כשהמנה מוכנה”, וממשיך לקבל הזמנות משולחנות אחרים. כשהמנה מוכנה — הוא חוזר, לוקח אותה ומגיש.
async ו-await
בדיוק בשביל זה יש await. בתוך ה-Middleware מופיעה השורה:
response = await call_next(request)call_next(request)— מעביר את הבקשה הלאה: ל-Route המתאים (או ל-404 אם אין כזה), ומחזיר את התשובה. זו ההזמנה למטבח.await— “תודיעו לי כשהמנה מוכנה”: מחכים לתשובה של ה-Route, ובינתיים השרת פנוי לטפל בבקשות אחרות. כשהתשובה מוכנה, הקוד ממשיך מהשורה הבאה.async def— אתawaitמותר לכתוב רק בתוך פונקציה שמוגדרת עםasync def. לכן ה-Middleware מוגדר כך.
רק את הדפוס הזה: async def למעלה, ו-await לפני call_next. ה-Routes שלנו נשארים def רגיל — FastAPI מטפל בהם בעצמו. איך השרת מנהל את ההמתנה מאחורי הקלעים (event loop, asyncio) — נלמד בהמשך המסלול.
request ו-response
| אובייקט | מה יש בו (מה שנשתמש בו) |
|---|---|
request.method | "GET", "POST"… |
request.url.path | הנתיב, למשל "/students/2" |
response.status_code | קוד התשובה: 200, 404… |
response.headers | ה-Headers של התשובה — עובד כמו מילון |
תחביר
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def my_middleware(request: Request, call_next):
# ... before the route
response = await call_next(request) # wait for the route's response
# ... after the route
return response # always pass the response on@app.middleware("http")— רושם את הפונקציה כ-Middleware לכל בקשת HTTP.request: Request—Requestמיובא מ-fastapi.return response— חובה. בלעדיו הלקוח לא מקבל תשובה.
דוגמת קוד ראשונה
לוג ומדידת זמן לכל בקשה ב-API הסטודנטים. בראש students_memory.py (מהשיעור הקודם) — רק מוסיפים, אף Route לא משתנה:
import logging
import time
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
app = FastAPI(title="Students API")
@app.middleware("http")
async def log_requests(request: Request, call_next):
start = time.time()
response = await call_next(request)
ms = round((time.time() - start) * 1000, 1)
logging.info(request.method + " " + request.url.path + " -> "
+ str(response.status_code) + " (" + str(ms) + " ms)")
return response
# ... students, StudentIn and all the routes stay exactly the sameמריצים (uvicorn students_memory:app --reload), ושולחים בקשות מ-Swagger (/docs). הלוג ב-Terminal:
INFO GET /students -> 200 (0.7 ms)
INFO GET /students/99 -> 404 (0.5 ms)
INFO POST /students -> 201 (1.3 ms)
INFO POST /students -> 400 (0.6 ms)
INFO GET /students/abc -> 422 (0.5 ms)פירוק הקוד שורה אחר שורה
| קוד | הסבר |
|---|---|
logging.basicConfig(...) | הגדרת הלוג, פעם אחת (נושא Python: Logging). |
@app.middleware("http") | הפונקציה הבאה תרוץ על כל בקשה. |
async def log_requests(request: Request, call_next): | הבקשה, ו-call_next — הדרך להמשיך ל-Route. |
start = time.time() | לפני ה-Route: מתחילים למדוד. |
response = await call_next(request) | מריצים את ה-Route ומחכים לתשובה. |
ms = round((time.time() - start) * 1000, 1) | אחרי: כמה זמן עבר, במילישניות. |
logging.info(...) | שורה אחת לכל בקשה: Method, נתיב, קוד וזמן. גם 404, 400 ו-422 — כי ה-Middleware רואה את התשובה הסופית, גם כשה-Route בכלל לא רץ. |
return response | מעבירים את התשובה ללקוח. |
שימו לב: הזמנים קצרים מאוד — הנתונים ברשימה בזיכרון. כשנחבר את השרת למסד נתונים, נראה את הזמנים עולים — ומדידה כזו היא בדיוק הדרך לגלות איפה השרת “מבזבז” זמן.
דוגמה נוספת
Header לכל תשובה
Middleware יכול גם לשנות את התשובה. כאן מוסיפים לכל תשובה Header בשם X-Process-Time, עם הזמן שלקח לטפל בה:
@app.middleware("http")
async def add_process_time(request: Request, call_next):
start = time.time()
response = await call_next(request)
ms = round((time.time() - start) * 1000, 1)
response.headers["X-Process-Time"] = str(ms)
return responseב-Swagger, תחת Response headers, מופיע עכשיו:
x-process-time: 0.7response.headers[...] = ...— כמו הוספת מפתח למילון.X-בתחילת השם — מוסכמה ל-Header שלנו, שאינו חלק מהתקן של HTTP.- אפשר כמה Middlewares באותו שרת — כל בקשה עוברת דרך כולם.
מתי לא Middleware?
Middleware מתאים לדברים שכל בקשה צריכה — לוג, מדידת זמן, Headers. כלל של Route אחד — למשל “גיל מינימלי 16” ב-POST /students — שייך ל-Route עצמו, ולא לקוד שרץ על כל השרת.
טעות נפוצה
הטעות 1: לשכוח return response.
@app.middleware("http")
async def log_requests(request: Request, call_next):
response = await call_next(request)
logging.info(request.url.path)ה-Middleware מחזיר None, והלקוח מקבל 500 — על כל בקשה, כי Middleware רץ על כולן.
הטעות 2: לשכוח await.
response = call_next(request)בלי await, לא מחכים ל-Route — ו-response הוא לא תשובה. השרת נכשל בכל בקשה. await לפני call_next, תמיד.
הטעות 3: def במקום async def. בלי async, אי אפשר להשתמש ב-await בתוך הפונקציה: SyntaxError: 'await' outside async function.
הטעות 4: לוגיקה של Route בתוך Middleware. if request.url.path == "/students" and ... — Middleware שבודק נתיבים ספציפיים הוא בדרך כלל סימן שהקוד שייך ל-Route.
תרגול
התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.
תרגיל 1: מה רץ קודם?
@app.middleware("http")
async def trace(request: Request, call_next):
print("before", request.method, request.url.path)
response = await call_next(request)
print("after", response.status_code)
return response
@app.get("/hello")
def hello():
print("inside the route")
return {"message": "hi"}שולחים GET /hello. מה יודפס, ובאיזה סדר? ומה יודפס עבור GET /nothing, נתיב שלא קיים?
הצג פתרון
from fastapi import FastAPI, Request
from fastapi.testclient import TestClient
app = FastAPI()
@app.middleware("http")
async def trace(request: Request, call_next):
print("before", request.method, request.url.path)
response = await call_next(request)
print("after", response.status_code)
return response
@app.get("/hello")
def hello():
print("inside the route")
return {"message": "hi"}
client = TestClient(app)
client.get("/hello")
client.get("/nothing")before GET /hello
inside the route
after 200
before GET /nothing
after 404- ה-Middleware רץ לפני ה-Route,
await call_next(request)מריץ את ה-Route, ואז ממשיכים אחרי — עם התשובה ביד. - גם בקשה לנתיב שלא קיים עוברת דרך ה-Middleware: אין Route שירוץ, ו-FastAPI מחזיר 404 — וה-Middleware רואה גם אותו.
תרגיל 2: לוג לכל בקשה
כתבו Middleware בשם log_requests שרושם ללוג, ברמת INFO, שורה לכל בקשה: ה-Method, הנתיב וקוד התשובה — בפורמט שבפלט הצפוי.
השתמשו באפליקציה הקטנה הזו: רשימה עם סטודנט אחד, GET /students ו-GET /students/{student_id} (שמחזיר 404 לסטודנט שלא קיים). את הלוג מגדירים כך שהשורות יודפסו למסך:
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s", stream=sys.stdout)
logging.getLogger("httpx").setLevel(logging.WARNING) # hide the test client's own log lines
students = [{"id": 1, "name": "Dana"}]כדי לבדוק בלי להריץ שרת, שולחים את שלוש הבקשות עם TestClient של FastAPI — כלי ששולח בקשות ישר לאפליקציה מתוך קוד Python:
from fastapi.testclient import TestClient
client = TestClient(app)
client.get("/students")
client.get("/students/1")
client.get("/students/99")פלט צפוי:
INFO GET /students -> 200
INFO GET /students/1 -> 200
INFO GET /students/99 -> 404הצג פתרון
import logging
import sys
from fastapi import FastAPI, HTTPException, Request
from fastapi.testclient import TestClient
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s", stream=sys.stdout)
logging.getLogger("httpx").setLevel(logging.WARNING) # hide the test client's own log lines
app = FastAPI()
students = [{"id": 1, "name": "Dana"}]
@app.middleware("http")
async def log_requests(request: Request, call_next):
response = await call_next(request)
logging.info(request.method + " " + request.url.path + " -> " + str(response.status_code))
return response
@app.get("/students")
def list_students():
return students
@app.get("/students/{student_id}")
def get_student(student_id: int):
for student in students:
if student["id"] == student_id:
return student
raise HTTPException(status_code=404, detail="student not found")
client = TestClient(app)
client.get("/students")
client.get("/students/1")
client.get("/students/99")INFO GET /students -> 200
INFO GET /students/1 -> 200
INFO GET /students/99 -> 404(stream=sys.stdout ושורת ה-httpx נדרשות רק כדי לבדוק עם TestClient. בשרת אמיתי מספיק basicConfig עם level ו-format.)
שורת לוג אחת, במקום אחד — ואף Route לא השתנה. גם Route שיתווסף מחר ייכנס ללוג אוטומטית.
תרגיל 3: Header עם זמן הטיפול
כתבו Middleware שמודד כמה זמן לקח לטפל בבקשה, ומוסיף לתשובה Header בשם X-Process-Time עם הזמן במילישניות. באפליקציה יש Route אחד, GET / שמחזיר {"message": "hello"}.
בדקו שה-Header מגיע — שולחים בקשה עם TestClient (כלי ששולח בקשות ישר לאפליקציה מתוך קוד Python) ובודקים את ה-Headers של התשובה:
from fastapi.testclient import TestClient
client = TestClient(app)
response = client.get("/")
print(response.status_code)
print("Has X-Process-Time:", "x-process-time" in response.headers)פלט צפוי:
200
Has X-Process-Time: Trueהצג פתרון
import time
from fastapi import FastAPI, Request
from fastapi.testclient import TestClient
app = FastAPI()
@app.middleware("http")
async def add_process_time(request: Request, call_next):
start = time.time()
response = await call_next(request)
ms = round((time.time() - start) * 1000, 1)
response.headers["X-Process-Time"] = str(ms)
return response
@app.get("/")
def home():
return {"message": "hello"}
client = TestClient(app)
response = client.get("/")
print(response.status_code)
print("Has X-Process-Time:", "x-process-time" in response.headers)200
Has X-Process-Time: Truetime.time()לפני ואחרי — בדיוק כמו ב-Decorator שמודד זמן.response.headersעובד כמו מילון. ה-Headers לא תלויים באותיות גדולות וקטנות, ולכןx-process-timeמוצא אתX-Process-Time.
תרגיל 4: async ו-await
@app.middleware("http")
async def log_requests(request: Request, call_next):
start = time.time()
response = await call_next(request)
ms = round((time.time() - start) * 1000, 1)
logging.info(request.url.path + " (" + str(ms) + " ms)")
return response- מה עושה השורה
response = await call_next(request)? הסבירו עם אנלוגיית המלצר והמטבח. - מה השרת עושה בזמן ש-
awaitמחכה? - למה הפונקציה מוגדרת עם
async defולא עםdef? - האם גם ה-Routes שלנו (
list_students,get_student) צריכים להיותasync def?
הצג פתרון
call_next(request)מעביר את הבקשה ל-Route — זו ההזמנה למטבח.awaitהוא “תודיעו לי כשהמנה מוכנה”: מחכים לתשובה של ה-Route, וכשהיא מוכנה הקוד ממשיך לשורה הבאה — עם התשובה ב-response.- מטפל בבקשות אחרות. כמו מלצר שלא עומד ליד המטבח אלא ממשיך לקבל הזמנות משולחנות אחרים — השרת לא “נתקע” על בקשה אחת.
- כי את
awaitמותר לכתוב רק בתוך פונקציה שמוגדרת עםasync def. עםdefרגיל, Python לא תאפשר את השורה בכלל:SyntaxError: 'await' outside async function. - לא. ה-Routes נשארים
defרגיל — FastAPI מטפל בהם בעצמו. בקורס הזהasyncו-awaitמופיעים רק ב-Middleware, בדפוס הזה.
תרגיל 5: מה לא בסדר?
@app.middleware("http")
def log_requests(request: Request, call_next):
response = call_next(request)
logging.info(request.url.path)מצאו שלוש בעיות. ומתי לא נכון להשתמש ב-Middleware?
הצג פתרון
defבמקוםasync def— Middleware ב-FastAPI חייב להיותasync def.- חסר
awaitלפניcall_next(request)— בלעדיו לא מחכים ל-Route, ו-responseהוא לא התשובה. - חסר
return response— ה-Middleware חייב להחזיר את התשובה הלאה. בלעדיו הלקוח לא מקבל כלום, והשרת נכשל.
מתי לא: Middleware מתאים לדברים שכל בקשה צריכה — לוג, מדידת זמן, Headers. כלל עסקי של Route אחד — למשל “גיל מינימלי 16” ב-POST /students — שייך ל-Route עצמו (או לפונקציית ולידציה), לא ל-Middleware שרץ על כל השרת.
סיכום
- Middleware = קוד שעוטף כל בקשה: לפני ה-Route, ואחריו. כמו Decorator — לכל השרת.
@app.middleware("http")+async def+await call_next(request)+return response.- לפני
call_next— מה שצריך לפני הבקשה (למשל, להתחיל למדוד). אחרי — יש תשובה ביד: קוד, Headers, זמן. - שימושים: לוג לכל בקשה, מדידת זמן, Headers לכל תשובה.
- כללים של Route אחד — ב-Route, לא ב-Middleware.
דוקומנטציה רשמית
- FastAPI — Middleware — @app.middleware(“http”) ו-call_next
- FastAPI — Concurrency and async / await — למה async ו-await