Accueil › L'ossature › Chapitre 13
HTTP, hosting et middleware
Un utilisateur clique. Deux cents millisecondes plus tard, une page s'affiche. Entre les deux, une dizaine d'étapes que ce chapitre déplie — parce que 90 % des bugs « bizarres » d'une application web viennent de l'ordre de ces étapes.
HTTP en cinq minutes
HTTP est un dialogue très codifié : le client envoie une requête, le serveur renvoie une réponse. Et surtout, il est sans mémoire : chaque requête ignore tout des précédentes (d'où les cookies et les jetons).
POST /api/commandes HTTP/1.1 ← verbe + chemin + version
Host: boutique.fr ← en-têtes : métadonnées
Content-Type: application/json
Authorization: Bearer eyJhbGciOi...
Accept-Language: fr-FR
{ "produitId": 42, "quantite": 2 } ← corps (facultatif)
HTTP/1.1 201 Created ← code de statut
Location: /api/commandes/1024 ← en-têtes de réponse
Content-Type: application/json
{ "id": 1024, "statut": "creee" } ← corps
Les verbes
GET | Lire. Sans effet de bord, cachable |
POST | Créer, ou déclencher une action |
PUT | Remplacer entièrement |
PATCH | Modifier partiellement |
DELETE | Supprimer |
GET, PUT et DELETE sont idempotents : les rejouer ne change rien de plus. POST non — d'où les doubles commandes quand un utilisateur clique deux fois.
Les codes qui comptent
200 / 201 / 204 | OK / créé / OK sans contenu |
301 / 302 / 304 | Déplacé / redirigé / non modifié |
400 | Requête invalide (ta faute, client) |
401 / 403 | Non authentifié / interdit |
404 / 409 / 422 | Introuvable / conflit / non traitable |
429 | Trop de requêtes |
500 / 502 / 503 | Bug serveur / passerelle / indisponible |
401 vs 403 : « je ne sais pas qui tu es » contre « je sais qui tu es, et tu n'as pas le droit ».
Le parcours complet d'une requête
Le middleware : une chaîne de contrôles
Les contrôles d'un aéroport. Enregistrement, contrôle des passeports, sécurité, porte d'embarquement. Chaque poste peut te laisser passer, t'ajouter quelque chose (une étiquette de bagage = un en-tête), ou t'arrêter net (pas de billet valide → tu ne verras jamais l'avion). Et au retour, on repasse les mêmes postes dans l'ordre inverse : c'est pourquoi un middleware de journalisation placé en premier voit à la fois la requête entrante et le statut final.
var builder = WebApplication.CreateBuilder(args);
// … enregistrement des services (voir chapitre 11) …
var app = builder.Build();
// ---------- 1. Gestion des erreurs : le plus à l'extérieur possible ----------
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();
else
{
app.UseExceptionHandler();
app.UseHsts();
}
// ---------- 2. Réseau / transport ----------
app.UseHttpsRedirection();
app.UseResponseCompression();
// ---------- 3. Fichiers statiques (avant le routage : inutile de router un .css) ----------
app.MapStaticAssets(); // .NET 9+ : compression et empreintes à la compilation
// ---------- 4. Routage ----------
app.UseRouting();
// ---------- 5. CORS — après le routage, avant l'authentification ----------
app.UseCors("front");
// ---------- 6. Qui es-tu ? puis : as-tu le droit ? ----------
app.UseAuthentication();
app.UseAuthorization(); // ⚠️ TOUJOURS après UseAuthentication
// ---------- 7. Limitation de débit, sessions, antiforgerie ----------
app.UseRateLimiter();
// ---------- 8. Les points de terminaison ----------
app.MapGet("/sante", () => Results.Ok("ok"));
app.MapControllers();
app.Run();
| Erreur d'ordre | Symptôme observé |
|---|---|
UseAuthorization avant UseAuthentication | Tout le monde est anonyme : 401 systématiques, ou User.Identity.Name vide |
UseCors après MapControllers | Le navigateur bloque : « No 'Access-Control-Allow-Origin' header » |
UseExceptionHandler en dernier | Une exception dans un middleware antérieur donne une page blanche ou une 500 brute |
Middleware ajouté après app.Run() | Il n'est jamais exécuté (le code après Run ne s'exécute qu'à l'arrêt) |
UseStaticFiles après l'authentification | Chaque image déclenche une vérification de jeton : lenteur inutile (voulu si les fichiers sont privés) |
Écrire son propre middleware
// Version courte, pour un besoin simple
app.Use(async (ctx, suivant) =>
{
var chrono = Stopwatch.StartNew();
ctx.Response.Headers["X-Trace-Id"] = ctx.TraceIdentifier;
await suivant(ctx); // ← passe la main au maillon suivant
// au RETOUR : la réponse existe déjà
app.Logger.LogInformation("{Methode} {Chemin} → {Statut} en {Ms}ms",
ctx.Request.Method, ctx.Request.Path, ctx.Response.StatusCode, chrono.ElapsedMilliseconds);
});
// Version en classe, testable et injectable
public class MiddlewareLocataire(RequestDelegate suivant)
{
public async Task InvokeAsync(HttpContext ctx, IContexteLocataire contexte) // ← DI possible ici
{
var id = ctx.Request.Headers["X-Locataire"].FirstOrDefault();
if (string.IsNullOrEmpty(id))
{
ctx.Response.StatusCode = StatusCodes.Status400BadRequest;
await ctx.Response.WriteAsJsonAsync(new { erreur = "En-tête X-Locataire manquant" });
return; // ← court-circuit : le reste du pipeline n'est PAS exécuté
}
contexte.Definir(id);
await suivant(ctx);
}
}
app.UseMiddleware<MiddlewareLocataire>();
Dès qu'un octet est envoyé au client, les en-têtes sont figés :
ctx.Response.Headers[...] = ... après un await suivant(ctx) qui a déjà écrit lève
« Headers are read-only, response has already started ». Modifie les en-têtes avant de passer la
main, ou utilise ctx.Response.OnStarting(...).
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments("/api"),
branche => branche.UseMiddleware<MiddlewareQuotas>());
app.Map("/admin", admin => { admin.UseMiddleware<MiddlewareAudit>(); /* … */ });
L'hôte : ce que fait WebApplication
var builder = WebApplication.CreateBuilder(args);
// Cette seule ligne met en place :
// • la configuration (appsettings, environnement, arguments, user-secrets)
// • la journalisation (console, débogueur)
// • le conteneur d'injection de dépendances
// • le serveur Kestrel
// • la détection de l'environnement (ASPNETCORE_ENVIRONMENT)
// Configurer le serveur
builder.WebHost.ConfigureKestrel(o =>
{
o.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10 Mo
o.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
});
var app = builder.Build();
app.Run(); // bloque jusqu'à l'arrêt du processus
Kestrel est un serveur HTTP complet, rapide, capable d'être exposé directement sur internet. On place malgré tout souvent un proxy inverse devant (nginx, IIS, Azure Front Door) pour : la terminaison TLS centralisée, la répartition de charge, le filtrage, la mise en cache, et la gestion de plusieurs applications sur un même port 443.
Dans ce cas, ton application ne voit plus l'adresse IP réelle du client mais celle du proxy. D'où :
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
// À placer TRÈS tôt, et à n'activer que si le proxy est de confiance :
// sinon un client peut usurper son adresse IP en envoyant l'en-tête lui-même.Démarrage et arrêt propres
// Une tâche de fond qui respecte l'arrêt demandé
public class NettoyageNocturne(IServiceScopeFactory fabrique, ILogger<NettoyageNocturne> log)
: BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken arret)
{
while (!arret.IsCancellationRequested)
{
using (var portee = fabrique.CreateScope()) // ← portée par cycle (chapitre 11)
{
var db = portee.ServiceProvider.GetRequiredService<AppDbContext>();
var supprimes = await db.Sessions
.Where(s => s.Expiration < DateTime.UtcNow)
.ExecuteDeleteAsync(arret);
log.LogInformation("{N} sessions expirées supprimées", supprimes);
}
await Task.Delay(TimeSpan.FromHours(1), arret); // ← le jeton interrompt l'attente
}
}
}
À l'arrêt (Ctrl+C, redéploiement, arrêt de conteneur), l'hôte annule le jeton, attend la fin des requêtes en cours pendant un délai de grâce, puis libère les services. C'est ce qui permet un déploiement sans requête interrompue — à condition de propager le jeton.
Vérifie que c'est passé
🎯 Quiz — 5 questions
1. Tes utilisateurs sont bien connectés, mais [Authorize] renvoie systématiquement 401.
Premier réflexe ?
2. Que signifie l'absence de await suivant(ctx) dans un middleware ?
3. Différence entre 401 et 403 ?
4. Où placer un middleware qui doit mesurer le temps total et voir le statut final ?
5. Ton application derrière nginx journalise toujours la même adresse IP. Pourquoi ?
X-Forwarded-For, mais .NET ne
l'exploite que si tu l'y autorises explicitement — par prudence, car cet en-tête est falsifiable.Fiches de révision
UseAuthentication() puis UseAuthorization(). Erreurs tout en haut.await suivant(ctx) : la réponse est produite sur place.BackgroundService + portée créée à chaque cycle + propagation du jeton d'arrêt.- HTTP est sans mémoire : chaque requête arrive nue, d'où cookies et jetons.
- Le pipeline est un aller-retour ordonné : l'ordre des
Use...est du code métier. - Erreurs d'abord, statiques avant routage, authentification avant autorisation, endpoints à la fin.
- Pas de
await suivant()= court-circuit assumé. - Derrière un proxy :
UseForwardedHeaders, et seulement si le proxy est de confiance.