# Middleware: код, который выполняется при каждом запросе<no value>

## Что мы изучим?

- Какая возникает проблема, когда нужно что-то делать при **каждом** запросе.
- Что такое **Middleware** и как оно связано с уже знакомым нам **декоратором**.
- **`@app.middleware("http")`**, **`call_next`**, **`async`** и **`await`**.
- **Лог** и **измерение времени** для каждого запроса в API студентов, а также **заголовок**, который добавляется к каждому ответу.

## Зачем это нужно?

API студентов из предыдущего урока работает. Теперь мы хотим знать, **что в нём происходит**: какой запрос пришёл, какой код вернулся и сколько это заняло. Первый способ, который приходит в голову, — добавить строку лога в каждый маршрут:

```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. **Одна и та же строка копируется** в каждый маршрут — пять, десять, пятьдесят раз.
2. **Новый маршрут, в котором строку забыли**, просто не логируется.
3. **Так нельзя узнать код ответа** — 404 и 400 создаются в `raise HTTPException`, после строки лога, а 422 создаётся ещё до выполнения маршрута. И нельзя узнать, сколько длился запрос.

## Объяснение с основ

### Идея — Middleware

**Middleware** (промежуточный слой) = код, который **оборачивает каждый запрос**, приходящий на сервер: он выполняется **до** маршрута, передаёт запрос дальше и выполняется снова **после** — когда ответ уже на руках.

```text
Client --request--> Middleware --------> Route (your function)
                    before: start the timer
Client <-response-- Middleware <-------- Route
                    after: status code, duration, log
```

### Как декоратор — для всего сервера

В уроке о декораторах мы написали декоратор, который измеряет время: он оборачивает **одну функцию**, выполняется до и после неё и вызывает её посередине. Middleware — ровно та же идея, только оно оборачивает **все запросы к серверу**, не трогая ни одного маршрута.

| | Декоратор | Middleware |
|---|---|---|
| Оборачивает | Одну функцию | Каждый запрос к серверу |
| Вызов посередине | `func(*args, **kwargs)` | `await call_next(request)` |
| Где добавляют | `@timed` над каждой функцией | Один раз, в `main.py` |

{{< box "analogy" "Аналогия: охранник на входе в здание" >}}
Каждый, кто входит в здание, проходит мимо охранника — неважно, на какой этаж он идёт. Охранник записывает, кто и когда вошёл и кто и когда вышел. Ему не нужно стоять у каждого офиса. **Middleware = охранник на входе**, а маршруты = офисы.
{{< /box >}}

### Почему нужно правильно ждать?

Middleware передаёт запрос маршруту, а затем **ждёт** ответа. Но сервер получает много запросов одновременно. Если он «застрянет» и будет ждать каждый запрос до конца, все остальные будут стоять в очереди.

{{< box "analogy" "Аналогия: официант и кухня" >}}
Официант передаёт заказ на кухню. Если он **стоит у кухни**, пока блюдо не будет готово, все остальные столики его ждут. Хороший официант говорит «**сообщите мне, когда блюдо будет готово**» и продолжает принимать заказы у других столиков. Когда блюдо готово — он возвращается, забирает его и подаёт.
{{< /box >}}

### `async` и `await`

Именно для этого и нужен **`await`**. Внутри Middleware есть строка:

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

- **`call_next(request)`** — передаёт запрос дальше: в подходящий маршрут (или в 404, если такого нет) и возвращает ответ. Это **заказ на кухню**.
- **`await`** — «**сообщите мне, когда блюдо будет готово**»: подождать ответа маршрута, а тем временем сервер **свободен** обрабатывать другие запросы. Когда ответ готов, код продолжается со следующей строки.
- **`async def`** — `await` можно писать **только** внутри функции, определённой через `async def`. Поэтому Middleware определяется так.

{{< box "tip" "Сколько об этом нужно знать на курсе?" >}}
Только этот шаблон: `async def` вверху и `await` перед `call_next`. Наши маршруты остаются обычным `def` — FastAPI сам их обрабатывает. Как сервер управляет ожиданием за кулисами (цикл событий, event loop, `asyncio`), мы изучим позже в треке.
{{< /box >}}

### `request` и `response`

| Объект | Что в нём (что будем использовать) |
|---|---|
| `request.method` | `"GET"`, `"POST"`... |
| `request.url.path` | Путь, например `"/students/2"` |
| `response.status_code` | Код ответа: `200`, `404`... |
| `response.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` (из предыдущего урока) — мы только добавляем, маршруты не меняются:

```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`). **Лог в терминале:**

```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(...)` | Настройка лога, один раз (уроки логирования). |
| `@app.middleware("http")` | Следующая функция будет выполняться при **каждом** запросе. |
| `async def log_requests(request: Request, call_next):` | Запрос и `call_next` — способ продолжить к маршруту. |
| `start = time.time()` | **До** маршрута: начинаем измерение. |
| `response = await call_next(request)` | Выполнить маршрут и дождаться ответа. |
| `ms = round((time.time() - start) * 1000, 1)` | **После**: сколько прошло времени, в миллисекундах. |
| `logging.info(...)` | Одна строка на каждый запрос: метод, путь, код и время. **Также 404, 400 и 422** — потому что Middleware видит окончательный ответ, даже когда маршрут вообще не выполнялся. |
| `return response` | Передать ответ клиенту. |

**Обратите внимание:** время очень короткое — данные лежат в списке в памяти. Когда мы подключим сервер к базе данных, увидим, как время растёт, — и такое измерение — именно способ обнаружить, где сервер «теряет» время.

## Ещё один пример

### Заголовок для каждого ответа

Middleware может и **менять** ответ. Здесь мы добавляем к каждому ответу заголовок с именем `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-`** в начале имени — соглашение для **собственного** заголовка, который не входит в стандарт HTTP.
- На одном сервере может быть несколько Middleware — каждый запрос проходит через все.

### Когда не Middleware?

Middleware подходит для того, что нужно **каждому** запросу, — лог, измерение времени, заголовки. Правило **одного маршрута** — например, «минимальный возраст 16» в `POST /students` — принадлежит самому маршруту, а не коду, который выполняется на всём сервере.

## Типичная ошибка

**Ошибка 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` вы не ждёте маршрута — и `response` не является ответом. Сервер падает на каждом запросе. **`await` перед `call_next` — всегда.**

**Ошибка 3: `def` вместо `async def`.** Без `async` внутри функции нельзя использовать `await`: `SyntaxError: 'await' outside async function`.

**Ошибка 4: логика маршрута внутри Middleware.** `if request.url.path == "/students" and ...` — Middleware, которое проверяет конкретные пути, обычно признак того, что код принадлежит маршруту.

<!-- 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 выполняется **до** маршрута, `await call_next(request)` запускает маршрут, а затем мы продолжаем **после** — с ответом на руках.
- Запрос к несуществующему пути тоже проходит через Middleware: маршрута для выполнения нет, FastAPI возвращает 404 — и Middleware это тоже видит.
{{< /solution >}}

### Упражнение 2: Лог для каждого запроса

Напишите Middleware с именем `log_requests`, которое пишет в лог на уровне INFO одну строку на каждый запрос: метод, путь и код ответа — в формате ожидаемого вывода.

Используйте это маленькое приложение: список с одним студентом, `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`.)

Одна строка лога в одном месте — и ни один маршрут не изменился. Маршрут, добавленный завтра, тоже автоматически попадёт в лог.
{{< /solution >}}

### Упражнение 3: Заголовок со временем обработки

Напишите Middleware, которое измеряет, сколько заняла обработка запроса, и добавляет к ответу заголовок с именем `X-Process-Time` со временем в миллисекундах. В приложении один маршрут — `GET /`, который возвращает `{"message": "hello"}`.

Проверьте, что заголовок приходит, — отправьте запрос через `TestClient` (инструмент, который отправляет запросы прямо в приложение из кода Python) и проверьте заголовки ответа:

```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()` до и после — точно как в декораторе, который измеряет время.
- `response.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. Должны ли наши маршруты (`list_students`, `get_student`) тоже быть `async def`?

{{< solution >}}
1. `call_next(request)` передаёт запрос маршруту — это **заказ на кухню**. `await` — «**сообщите мне, когда блюдо будет готово**»: подождать ответа маршрута, и когда он готов, код продолжается на следующей строке — с ответом в `response`.
2. **Обрабатывает другие запросы.** Как официант, который не стоит у кухни, а продолжает принимать заказы у других столиков, — сервер не «застревает» на одном запросе.
3. Потому что `await` **можно писать только** внутри функции, определённой через `async def`. С обычным `def` Python вообще не допустит эту строку: `SyntaxError: 'await' outside async function`.
4. **Нет.** Маршруты остаются обычным `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)` — без него вы не ждёте маршрута, и `response` — не ответ.
3. **Не хватает `return response`** — Middleware должно вернуть ответ дальше. Без этого клиент ничего не получит, и сервер падает.

**Когда не нужно:** Middleware подходит для того, что нужно **каждому** запросу, — лог, измерение времени, заголовки. Бизнес-правило одного маршрута — например, «минимальный возраст 16» в `POST /students` — принадлежит самому маршруту (или функции проверки), а не Middleware, которое выполняется на всём сервере.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- **Middleware** = код, который оборачивает **каждый** запрос: до маршрута и после него. Как декоратор — для всего сервера.
- **`@app.middleware("http")`** + **`async def`** + **`await call_next(request)`** + **`return response`**.
- **До** `call_next` — то, что нужно перед запросом (например, начать измерение). **После** — ответ на руках: код, заголовки, время.
- Применения: **лог** для каждого запроса, **измерение времени**, **заголовки** для каждого ответа.
- Правила одного маршрута — в маршруте, а не в Middleware.

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

## Официальная документация

- [FastAPI — Middleware](https://fastapi.tiangolo.com/tutorial/middleware/)
- [FastAPI — Concurrency and async / await](https://fastapi.tiangolo.com/async/)

<!-- docs:end -->
