# Middleware — קוד שרץ על כל בקשה<no value>

## מה נלמד?

- איזו בעיה יש כשרוצים לעשות משהו **בכל** בקשה.
- מה זה **Middleware**, ואיך הוא קשור ל-**Decorator** שכבר מכירים.
- **`@app.middleware("http")`**, **`call_next`**, **`async`** ו-**`await`**.
- **לוג** ו**מדידת זמן** לכל בקשה ב-API הסטודנטים, ו-**Header** שמתווסף לכל תשובה.

## למה צריך את זה?

ה-API הסטודנטים מהשיעור הקודם עובד. עכשיו רוצים לדעת **מה קורה בו**: איזו בקשה הגיעה, איזה קוד חזר, וכמה זמן היא לקחה. הדרך הראשונה שעולה לראש — להוסיף שורת לוג לכל Route:

```python
@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))
    ...
```

שלוש בעיות:

1. **אותה שורה מועתקת** לכל Route — חמש, עשר, חמישים פעמים.
2. **Route חדש שבו שכחו את השורה** — פשוט לא נרשם.
3. **אין דרך לדעת כך את קוד התשובה** — 404 ו-400 נוצרים ב-`raise HTTPException`, אחרי שורת הלוג, ו-422 נוצר עוד לפני שה-Route רץ. וגם לא כמה זמן הבקשה לקחה.

## הסבר מהבסיס

### הרעיון — Middleware

**Middleware** (תוכנת ביניים) = קוד ש**עוטף כל בקשה** שמגיעה לשרת: הוא רץ **לפני** ה-Route, מעביר את הבקשה הלאה, ורץ שוב **אחרי** — כשהתשובה כבר ביד.

```text
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` |

{{< box "analogy" "אנלוגיה: השומר בכניסה לבניין" >}}
כל מי שנכנס לבניין עובר ליד השומר — לא משנה לאיזו קומה הוא הולך. השומר רושם מי נכנס ומתי, ומי יצא ומתי. הוא לא צריך לעמוד ליד כל משרד. **Middleware = השומר בכניסה**, וה-Routes = המשרדים.
{{< /box >}}

### למה צריך לחכות נכון?

ה-Middleware מעביר את הבקשה ל-Route, ואז **מחכה** לתשובה. אבל שרת מקבל הרבה בקשות בבת אחת. אם הוא "נתקע" ומחכה לכל בקשה עד הסוף, כל השאר עומדות בתור.

{{< box "analogy" "אנלוגיה: מלצר ומטבח" >}}
מלצר מעביר הזמנה למטבח. אם הוא **עומד ליד המטבח** עד שהמנה מוכנה — כל השולחנות האחרים מחכים לו. מלצר טוב אומר "**תודיעו לי כשהמנה מוכנה**", וממשיך לקבל הזמנות משולחנות אחרים. כשהמנה מוכנה — הוא חוזר, לוקח אותה ומגיש.
{{< /box >}}

### `async` ו-`await`

בדיוק בשביל זה יש **`await`**. בתוך ה-Middleware מופיעה השורה:

```python
response = await call_next(request)
```

- **`call_next(request)`** — מעביר את הבקשה הלאה: ל-Route המתאים (או ל-404 אם אין כזה), ומחזיר את התשובה. זו **ההזמנה למטבח**.
- **`await`** — "**תודיעו לי כשהמנה מוכנה**": מחכים לתשובה של ה-Route, ובינתיים השרת **פנוי** לטפל בבקשות אחרות. כשהתשובה מוכנה, הקוד ממשיך מהשורה הבאה.
- **`async def`** — את `await` מותר לכתוב **רק** בתוך פונקציה שמוגדרת עם `async def`. לכן ה-Middleware מוגדר כך.

{{< box "tip" "כמה צריך לדעת על זה בקורס?" >}}
רק את הדפוס הזה: `async def` למעלה, ו-`await` לפני `call_next`. ה-Routes שלנו נשארים `def` רגיל — FastAPI מטפל בהם בעצמו. איך השרת מנהל את ההמתנה מאחורי הקלעים (event loop, `asyncio`) — נלמד בהמשך המסלול.
{{< /box >}}

### `request` ו-`response`

| אובייקט | מה יש בו (מה שנשתמש בו) |
|---|---|
| `request.method` | `"GET"`, `"POST"`... |
| `request.url.path` | הנתיב, למשל `"/students/2"` |
| `response.status_code` | קוד התשובה: `200`, `404`... |
| `response.headers` | ה-Headers של התשובה — עובד כמו מילון |

## תחביר

```python
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 לא משתנה:

```python
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:**

```text
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`, עם הזמן שלקח לטפל בה:

```python
@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**, מופיע עכשיו:

```text
x-process-time: 0.7
```

- **`response.headers[...] = ...`** — כמו הוספת מפתח למילון.
- **`X-`** בתחילת השם — מוסכמה ל-Header **שלנו**, שאינו חלק מהתקן של HTTP.
- אפשר כמה Middlewares באותו שרת — כל בקשה עוברת דרך כולם.

### מתי לא Middleware?

Middleware מתאים לדברים ש**כל** בקשה צריכה — לוג, מדידת זמן, Headers. כלל של **Route אחד** — למשל "גיל מינימלי 16" ב-`POST /students` — שייך ל-Route עצמו, ולא לקוד שרץ על כל השרת.

## טעות נפוצה

**הטעות 1: לשכוח `return response`.**

```python
@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`.**

```python
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.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## תרגול

התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.

### תרגיל 1: מה רץ קודם?

```text
@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`, נתיב שלא קיים?

{{< solution >}}
```python
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")
```

```text
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 רואה גם אותו.
{{< /solution >}}

### תרגיל 2: לוג לכל בקשה

כתבו Middleware בשם `log_requests` שרושם ללוג, ברמת INFO, שורה לכל בקשה: ה-Method, הנתיב וקוד התשובה — בפורמט שבפלט הצפוי.

השתמשו באפליקציה הקטנה הזו: רשימה עם סטודנט אחד, `GET /students` ו-`GET /students/{student_id}` (שמחזיר 404 לסטודנט שלא קיים). את הלוג מגדירים כך שהשורות יודפסו למסך:

```py
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:

```py
from fastapi.testclient import TestClient

client = TestClient(app)
client.get("/students")
client.get("/students/1")
client.get("/students/99")
```

**פלט צפוי:**

```text
INFO GET /students -> 200
INFO GET /students/1 -> 200
INFO GET /students/99 -> 404
```

{{< solution >}}
```python
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")
```

```text
INFO GET /students -> 200
INFO GET /students/1 -> 200
INFO GET /students/99 -> 404
```

(`stream=sys.stdout` ושורת ה-`httpx` נדרשות רק כדי לבדוק עם `TestClient`. בשרת אמיתי מספיק `basicConfig` עם `level` ו-`format`.)

שורת לוג אחת, במקום אחד — ואף Route לא השתנה. גם Route שיתווסף מחר ייכנס ללוג אוטומטית.
{{< /solution >}}

### תרגיל 3: Header עם זמן הטיפול

כתבו Middleware שמודד כמה זמן לקח לטפל בבקשה, ומוסיף לתשובה Header בשם `X-Process-Time` עם הזמן במילישניות. באפליקציה יש Route אחד, `GET /` שמחזיר `{"message": "hello"}`.

בדקו שה-Header מגיע — שולחים בקשה עם `TestClient` (כלי ששולח בקשות ישר לאפליקציה מתוך קוד Python) ובודקים את ה-Headers של התשובה:

```py
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)
```

**פלט צפוי:**

```text
200
Has X-Process-Time: True
```

{{< solution >}}
```python
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)
```

```text
200
Has X-Process-Time: True
```

- `time.time()` לפני ואחרי — בדיוק כמו ב-Decorator שמודד זמן.
- `response.headers` עובד כמו מילון. ה-Headers לא תלויים באותיות גדולות וקטנות, ולכן `x-process-time` מוצא את `X-Process-Time`.
{{< /solution >}}

### תרגיל 4: async ו-await

```text
@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
```

1. מה עושה השורה `response = await call_next(request)`? הסבירו עם אנלוגיית המלצר והמטבח.
2. מה השרת עושה בזמן ש-`await` מחכה?
3. למה הפונקציה מוגדרת עם `async def` ולא עם `def`?
4. האם גם ה-Routes שלנו (`list_students`, `get_student`) צריכים להיות `async def`?

{{< solution >}}
1. `call_next(request)` מעביר את הבקשה ל-Route — זו **ההזמנה למטבח**. `await` הוא "**תודיעו לי כשהמנה מוכנה**": מחכים לתשובה של ה-Route, וכשהיא מוכנה הקוד ממשיך לשורה הבאה — עם התשובה ב-`response`.
2. **מטפל בבקשות אחרות.** כמו מלצר שלא עומד ליד המטבח אלא ממשיך לקבל הזמנות משולחנות אחרים — השרת לא "נתקע" על בקשה אחת.
3. כי את `await` **מותר לכתוב רק** בתוך פונקציה שמוגדרת עם `async def`. עם `def` רגיל, Python לא תאפשר את השורה בכלל: `SyntaxError: 'await' outside async function`.
4. **לא.** ה-Routes נשארים `def` רגיל — FastAPI מטפל בהם בעצמו. בקורס הזה `async` ו-`await` מופיעים רק ב-Middleware, בדפוס הזה.
{{< /solution >}}

### תרגיל 5: מה לא בסדר?

```text
@app.middleware("http")
def log_requests(request: Request, call_next):
    response = call_next(request)
    logging.info(request.url.path)
```

מצאו שלוש בעיות. ומתי **לא** נכון להשתמש ב-Middleware?

{{< solution >}}
1. **`def` במקום `async def`** — Middleware ב-FastAPI חייב להיות `async def`.
2. **חסר `await`** לפני `call_next(request)` — בלעדיו לא מחכים ל-Route, ו-`response` הוא לא התשובה.
3. **חסר `return response`** — ה-Middleware חייב להחזיר את התשובה הלאה. בלעדיו הלקוח לא מקבל כלום, והשרת נכשל.

**מתי לא:** Middleware מתאים לדברים ש**כל** בקשה צריכה — לוג, מדידת זמן, Headers. כלל עסקי של Route אחד — למשל "גיל מינימלי 16" ב-`POST /students` — שייך ל-Route עצמו (או לפונקציית ולידציה), לא ל-Middleware שרץ על כל השרת.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **Middleware** = קוד שעוטף **כל** בקשה: לפני ה-Route, ואחריו. כמו Decorator — לכל השרת.
- **`@app.middleware("http")`** + **`async def`** + **`await call_next(request)`** + **`return response`**.
- **לפני** `call_next` — מה שצריך לפני הבקשה (למשל, להתחיל למדוד). **אחרי** — יש תשובה ביד: קוד, Headers, זמן.
- שימושים: **לוג** לכל בקשה, **מדידת זמן**, **Headers** לכל תשובה.
- כללים של Route אחד — ב-Route, לא ב-Middleware.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## דוקומנטציה רשמית

- [FastAPI — Middleware](https://fastapi.tiangolo.com/tutorial/middleware/) — @app.middleware("http") ו-call_next
- [FastAPI — Concurrency and async / await](https://fastapi.tiangolo.com/async/) — למה async ו-await

<!-- docs:end -->
