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

Une requête HTTP, telle qu'elle circule
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)
La réponse
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

GETLire. Sans effet de bord, cachable
POSTCréer, ou déclencher une action
PUTRemplacer entièrement
PATCHModifier partiellement
DELETESupprimer

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 / 204OK / créé / OK sans contenu
301 / 302 / 304Déplacé / redirigé / non modifié
400Requête invalide (ta faute, client)
401 / 403Non authentifié / interdit
404 / 409 / 422Introuvable / conflit / non traitable
429Trop de requêtes
500 / 502 / 503Bug 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

Navigateur clic DNS + TLS résolution du nom chiffrement HTTPS Proxy inverse nginx, IIS, Azure (souvent, pas toujours) Kestrel le serveur .NET construit HttpContext PIPELINE DE MIDDLEWARES — chacun peut agir, passer la main, ou répondre tout de suite Exceptions HTTPS Statiques Routing AuthN AuthZ → aller : chaque maillon voit la requête ← retour : chaque maillon voit la réponse (dans l'ordre inverse) TON CODE (endpoint, contrôleur, composant)

Le middleware : une chaîne de contrôles

L'image

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.

Program.cs — l'ordre canonique, à ne pas improviser
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();
Les inversions classiques et leurs symptômes
Erreur d'ordreSymptôme observé
UseAuthorization avant UseAuthenticationTout le monde est anonyme : 401 systématiques, ou User.Identity.Name vide
UseCors après MapControllersLe navigateur bloque : « No 'Access-Control-Allow-Origin' header »
UseExceptionHandler en dernierUne 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'authentificationChaque 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>();
Une fois la réponse commencée, on ne revient pas

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(...).

Brancher un middleware sur une portion du site seulement
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 et le proxy inverse

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 ?

Sans authentification préalable, aucune identité n'est établie : l'autorisation évalue un utilisateur anonyme. C'est l'erreur d'ordre la plus fréquente.

2. Que signifie l'absence de await suivant(ctx) dans un middleware ?

C'est exactement ce que fait un middleware de cache ou de rejet : répondre immédiatement sans déranger la suite du pipeline.

3. Différence entre 401 et 403 ?

401 invite à se connecter ; 403 dit que se reconnecter ne changera rien.

4. Où placer un middleware qui doit mesurer le temps total et voir le statut final ?

Le pipeline est un aller-retour. Le premier maillon à l'aller est le dernier au retour : il englobe tout le traitement.

5. Ton application derrière nginx journalise toujours la même adresse IP. Pourquoi ?

Le proxy transmet l'IP d'origine dans 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

Middleware, en une image ?
Les contrôles d'un aéroport : chacun agit, passe la main, ou arrête. Retour dans l'ordre inverse.
L'ordre à ne jamais inverser ?
UseAuthentication() puis UseAuthorization(). Erreurs tout en haut.
Court-circuiter ?
Ne pas appeler await suivant(ctx) : la réponse est produite sur place.
401 vs 403 ?
401 : je ne sais pas qui tu es. 403 : je le sais, et c'est non.
Kestrel ?
Le serveur HTTP intégré. Souvent derrière un proxy inverse pour TLS et répartition de charge.
Tâche de fond ?
BackgroundService + portée créée à chaque cycle + propagation du jeton d'arrêt.
À retenir