# Le middleware : du code qui s'exécute à chaque requête<no value>

## Qu'allons-nous apprendre ?

- Quel problème se pose quand on veut faire quelque chose à **chaque** requête.
- Ce qu'est un **middleware**, et quel est son lien avec le **décorateur** que nous connaissons déjà.
- **`@app.middleware("http")`**, **`call_next`**, **`async`** et **`await`**.
- Un **journal** et une **mesure du temps** pour chaque requête de l'API des étudiants, et un **en-tête** (Header) ajouté à chaque réponse.

## Pourquoi en avons-nous besoin ?

L'API des étudiants de la leçon précédente fonctionne. Maintenant, nous voulons savoir **ce qui s'y passe** : quelle requête est arrivée, quel code est revenu, et combien de temps cela a pris. La première idée qui vient à l'esprit — ajouter une ligne de journal dans chaque 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))
    ...
```

Trois problèmes :

1. **La même ligne est copiée** dans chaque route — cinq, dix, cinquante fois.
2. **Une nouvelle route où l'on a oublié la ligne** — n'est tout simplement pas journalisée.
3. **On ne peut pas connaître ainsi le code de réponse** — les 404 et 400 sont créés dans `raise HTTPException`, après la ligne de journal, et le 422 est créé avant même que la route ne s'exécute. Ni combien de temps a pris la requête.

## Explication à partir des bases

### L'idée — le middleware

**Middleware** = du code qui **enveloppe chaque requête** arrivant au serveur : il s'exécute **avant** la route, transmet la requête, et s'exécute de nouveau **après** — quand la réponse est déjà là.

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

### Comme un décorateur — pour tout le serveur

Dans la leçon sur les décorateurs, nous avons écrit un décorateur qui mesure le temps : il enveloppe **une seule fonction**, s'exécute avant et après elle, et l'appelle au milieu. Le middleware est exactement la même idée — sauf qu'il enveloppe **toutes les requêtes vers le serveur**, sans toucher à aucune route.

| | Décorateur | Middleware |
|---|---|---|
| Enveloppe | Une seule fonction | Chaque requête vers le serveur |
| L'appel au milieu | `func(*args, **kwargs)` | `await call_next(request)` |
| Où on l'ajoute | `@timed` au-dessus de chaque fonction | Une seule fois, dans `main.py` |

{{< box "analogy" "Analogie : le gardien à l'entrée de l'immeuble" >}}
Tous ceux qui entrent dans l'immeuble passent devant le gardien — quel que soit l'étage où ils vont. Le gardien note qui est entré et quand, et qui est sorti et quand. Il n'a pas besoin de se tenir à côté de chaque bureau. **Middleware = le gardien à l'entrée**, et les routes = les bureaux.
{{< /box >}}

### Pourquoi faut-il bien attendre ?

Le middleware transmet la requête à la route, puis **attend** la réponse. Mais un serveur reçoit de nombreuses requêtes en même temps. S'il « se bloque » et attend chaque requête jusqu'à la fin, toutes les autres font la queue.

{{< box "analogy" "Analogie : un serveur de restaurant et une cuisine" >}}
Un serveur transmet une commande à la cuisine. S'il **reste planté à côté de la cuisine** jusqu'à ce que le plat soit prêt — toutes les autres tables l'attendent. Un bon serveur dit « **prévenez-moi quand le plat est prêt** », et continue de prendre les commandes des autres tables. Quand le plat est prêt — il revient, le prend et le sert.
{{< /box >}}

### `async` et `await`

C'est exactement à cela que sert **`await`**. Dans le middleware, il y a la ligne :

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

- **`call_next(request)`** — transmet la requête : à la route correspondante (ou à un 404 s'il n'y en a pas), et renvoie la réponse. C'est **la commande à la cuisine**.
- **`await`** — « **prévenez-moi quand le plat est prêt** » : attendre la réponse de la route, et pendant ce temps le serveur est **libre** de traiter d'autres requêtes. Quand la réponse est prête, le code continue à la ligne suivante.
- **`async def`** — `await` ne peut s'écrire **qu'**à l'intérieur d'une fonction définie avec `async def`. C'est pourquoi le middleware est défini ainsi.

{{< box "tip" "Que faut-il en savoir dans ce cours ?" >}}
Seulement ce modèle : `async def` en haut, et `await` avant `call_next`. Nos routes restent un `def` ordinaire — FastAPI s'en occupe tout seul. Comment le serveur gère l'attente en coulisses (la boucle d'événements, `asyncio`) — nous l'apprendrons plus tard dans le parcours.
{{< /box >}}

### `request` et `response`

| Objet | Ce qu'il contient (ce que nous utiliserons) |
|---|---|
| `request.method` | `"GET"`, `"POST"`... |
| `request.url.path` | Le chemin, par exemple `"/students/2"` |
| `response.status_code` | Le code de réponse : `200`, `404`... |
| `response.headers` | Les en-têtes de la réponse — fonctionne comme un dictionnaire |

## Syntaxe

```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")`** — enregistre la fonction comme middleware pour chaque requête HTTP.
- **`request: Request`** — `Request` s'importe depuis `fastapi`.
- **`return response`** — obligatoire. Sans lui, le client ne reçoit pas de réponse.

## Premier exemple de code

**Un journal et une mesure du temps pour chaque requête de l'API des étudiants.** En haut de `students_memory.py` (de la leçon précédente) — nous ne faisons qu'ajouter, aucune route ne change :

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

Lancez-le (`uvicorn students_memory:app --reload`), et envoyez des requêtes depuis Swagger (`/docs`). **Le journal dans le 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)
```

## Analyse du code ligne par ligne

| Code | Explication |
|---|---|
| `logging.basicConfig(...)` | La configuration du journal, une seule fois (les leçons sur la journalisation). |
| `@app.middleware("http")` | La fonction suivante s'exécutera à **chaque** requête. |
| `async def log_requests(request: Request, call_next):` | La requête, et `call_next` — le moyen de continuer vers la route. |
| `start = time.time()` | **Avant** la route : démarrer la mesure. |
| `response = await call_next(request)` | Exécuter la route et attendre la réponse. |
| `ms = round((time.time() - start) * 1000, 1)` | **Après** : combien de temps s'est écoulé, en millisecondes. |
| `logging.info(...)` | Une ligne pour chaque requête : la méthode, le chemin, le code et le temps. **Aussi les 404, 400 et 422** — car le middleware voit la réponse finale, même quand la route ne s'est pas exécutée du tout. |
| `return response` | Transmettre la réponse au client. |

**Remarquez :** les temps sont très courts — les données sont dans une liste en mémoire. Quand nous connecterons le serveur à une base de données, nous verrons les temps augmenter — et une mesure comme celle-ci est exactement le moyen de découvrir où le serveur « perd » du temps.

## Un autre exemple

### Un en-tête pour chaque réponse

Un middleware peut aussi **modifier** la réponse. Ici, nous ajoutons à chaque réponse un en-tête nommé `X-Process-Time`, avec le temps qu'a pris son traitement :

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

Dans Swagger, sous **Response headers**, ceci apparaît maintenant :

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

- **`response.headers[...] = ...`** — comme ajouter une clé à un dictionnaire.
- **`X-`** au début du nom — une convention pour un en-tête **à nous**, qui ne fait pas partie de la norme HTTP.
- On peut avoir plusieurs middlewares dans le même serveur — chaque requête passe par tous.

### Quand ne pas utiliser de middleware ?

Le middleware convient aux choses dont **chaque** requête a besoin — un journal, une mesure du temps, des en-têtes. Une règle propre à **une seule route** — par exemple « âge minimum 16 » dans `POST /students` — appartient à la route elle-même, et non à du code qui s'exécute sur tout le serveur.

## Erreur courante

**Erreur 1 : oublier `return response`.**

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

Le middleware renvoie `None`, et le client reçoit un **500** — à **chaque** requête, car le middleware s'exécute sur toutes.

**Erreur 2 : oublier `await`.**

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

Sans `await`, on n'attend pas la route — et `response` n'est pas une réponse. Le serveur échoue à chaque requête. **`await` avant `call_next`, toujours.**

**Erreur 3 : `def` au lieu de `async def`.** Sans `async`, on ne peut pas utiliser `await` dans la fonction : `SyntaxError: 'await' outside async function`.

**Erreur 4 : de la logique de route dans un middleware.** `if request.url.path == "/students" and ...` — un middleware qui vérifie des chemins précis est généralement le signe que le code appartient à une route.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Exercices

Les exercices n'utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d'abord seul, et n'ouvrez la solution qu'ensuite.

### Exercice 1: Qu'est-ce qui s'exécute en premier ?

```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"}
```

Vous envoyez `GET /hello`. Qu'est-ce qui sera affiché, et dans quel ordre ? Et qu'est-ce qui sera affiché pour `GET /nothing`, un chemin qui n'existe pas ?

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

- Le middleware s'exécute **avant** la route, `await call_next(request)` exécute la route, puis on continue **après** — avec la réponse en main.
- Une requête vers un chemin qui n'existe pas passe aussi par le middleware : il n'y a pas de route à exécuter, et FastAPI renvoie 404 — et le middleware le voit aussi.
{{< /solution >}}

### Exercice 2: Un journal pour chaque requête

Écrivez un middleware nommé `log_requests` qui écrit dans le journal, au niveau INFO, une ligne pour chaque requête : la méthode, le chemin et le code de réponse — au format du résultat attendu.

Utilisez cette petite application : une liste avec un étudiant, `GET /students` et `GET /students/{student_id}` (qui renvoie 404 pour un étudiant qui n'existe pas). Configurez le journal pour que les lignes s'affichent à l'écran :

```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"}]
```

Pour tester sans lancer de serveur, envoyez les trois requêtes avec le `TestClient` de FastAPI — un outil qui envoie des requêtes directement à l'application depuis du code Python :

```py
from fastapi.testclient import TestClient

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

**Résultat attendu :**

```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` et la ligne `httpx` ne sont nécessaires que pour tester avec `TestClient`. Sur un vrai serveur, `basicConfig` avec `level` et `format` suffit.)

Une seule ligne de journal, à un seul endroit — et aucune route n'a changé. Une route ajoutée demain entrera aussi automatiquement dans le journal.
{{< /solution >}}

### Exercice 3: Un en-tête avec le temps de traitement

Écrivez un middleware qui mesure combien de temps a pris le traitement de la requête, et ajoute à la réponse un en-tête nommé `X-Process-Time` avec le temps en millisecondes. L'application a une seule route, `GET /` qui renvoie `{"message": "hello"}`.

Vérifiez que l'en-tête arrive — envoyez une requête avec `TestClient` (un outil qui envoie des requêtes directement à l'application depuis du code Python) et vérifiez les en-têtes de la réponse :

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

**Résultat attendu :**

```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()` avant et après — exactement comme dans le décorateur qui mesure le temps.
- `response.headers` fonctionne comme un dictionnaire. Les en-têtes ne dépendent pas des majuscules et minuscules, donc `x-process-time` trouve `X-Process-Time`.
{{< /solution >}}

### Exercice 4: async et 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. Que fait la ligne `response = await call_next(request)` ? Expliquez avec l'analogie du serveur de restaurant et de la cuisine.
2. Que fait le serveur pendant que `await` attend ?
3. Pourquoi la fonction est-elle définie avec `async def` et non avec `def` ?
4. Nos routes (`list_students`, `get_student`) doivent-elles aussi être `async def` ?

{{< solution >}}
1. `call_next(request)` transmet la requête à la route — c'est **la commande à la cuisine**. `await`, c'est « **prévenez-moi quand le plat est prêt** » : attendre la réponse de la route, et quand elle est prête, le code continue à la ligne suivante — avec la réponse dans `response`.
2. **Il traite d'autres requêtes.** Comme un serveur qui ne reste pas planté à côté de la cuisine mais continue de prendre les commandes des autres tables — le serveur ne « se bloque » pas sur une seule requête.
3. Parce que `await` **ne peut s'écrire qu'**à l'intérieur d'une fonction définie avec `async def`. Avec un `def` ordinaire, Python n'acceptera même pas la ligne : `SyntaxError: 'await' outside async function`.
4. **Non.** Les routes restent un `def` ordinaire — FastAPI s'en occupe tout seul. Dans ce cours, `async` et `await` n'apparaissent que dans le middleware, selon ce modèle.
{{< /solution >}}

### Exercice 5: Qu'est-ce qui ne va pas ?

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

Trouvez trois problèmes. Et quand n'est-il **pas** judicieux d'utiliser un middleware ?

{{< solution >}}
1. **`def` au lieu de `async def`** — un middleware dans FastAPI doit être `async def`.
2. **Il manque `await`** avant `call_next(request)` — sans lui, on n'attend pas la route, et `response` n'est pas la réponse.
3. **Il manque `return response`** — le middleware doit renvoyer la réponse plus loin. Sans cela, le client ne reçoit rien, et le serveur échoue.

**Quand ne pas l'utiliser :** le middleware convient aux choses dont **chaque** requête a besoin — un journal, une mesure du temps, des en-têtes. Une règle métier d'une seule route — par exemple « âge minimum 16 » dans `POST /students` — appartient à la route elle-même (ou à une fonction de validation), pas à un middleware qui s'exécute sur tout le serveur.
{{< /solution >}}

<!-- exercises:end -->

## Résumé

- **Middleware** = du code qui enveloppe **chaque** requête : avant la route, et après. Comme un décorateur — pour tout le serveur.
- **`@app.middleware("http")`** + **`async def`** + **`await call_next(request)`** + **`return response`**.
- **Avant** `call_next` — ce qu'il faut avant la requête (par exemple, démarrer la mesure). **Après** — on a la réponse en main : le code, les en-têtes, le temps.
- Usages : un **journal** pour chaque requête, la **mesure du temps**, des **en-têtes** pour chaque réponse.
- Les règles d'une seule route — dans la route, pas dans le middleware.

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

## Documentation officielle

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

<!-- docs:end -->
