Aller au contenu principal

Backend pratique · L2 · Section 1/6

HTTP et serveurs

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#HTTP et serveurs

Exposez une API claire : des routes explicites, des statuts cohérents et des erreurs JSON structurées. Une requête se termine toujours proprement, même en cas d’exception : le client reçoit un corps stable avec un code de statut adapté et un identifiant de corrélation pour l’investigation.

Avant de continuer, vous devez savoir lire une requête HTTP (méthode, chemin, en-têtes, corps) et écrire du JavaScript côté serveur (Node avec Express ou équivalent). À la fin de ce chapitre, vous saurez fiabiliser un endpoint de bout en bout : validation, erreur exploitable, timeout, limitation de débit, arrêt propre.

#Anatomie d’un échange complet

Un POST /signup bien élevé, observé au fil du réseau :

httphttp

1POST /signup HTTP/1.12Host: api.example.com3Content-Type: application/json; charset=utf-84Accept: application/json5 6{"email": "nour@example.com", "password": "correct horse battery"}
httphttp

1HTTP/1.1 201 Created2Content-Type: application/json; charset=utf-83Location: /users/424Set-Cookie: sid=8f3a…; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=18005X-Request-ID: 4f9c2a1e-b3d7-4c55-9a2f-1e6d8b0a7c336 7{"id": 42}

Trois lectures à faire sur cet échange : le type du corps accepté est annoncé (Content-Type avec jeu de caractères), la ressource créée est désignée (Location), et la session part dans un cookie inaccessible au JavaScript (HttpOnly), transmis seulement en HTTPS (Secure). Le X-Request-ID se retrouvera dans les logs du serveur : c’est le fil qui relie un incident à ses traces.

Le même endpoint qui échoue reste exploitable :

httphttp

1HTTP/1.1 409 Conflict2Content-Type: application/problem+json3X-Request-ID: 2b7e0c45-…4 5{6  "type": "https://example.com/errors/email-taken",7  "title": "Adresse déjà utilisée",8  "status": 409,9  "detail": "Un compte existe déjà avec cette adresse.",10  "traceId": "2b7e0c45-…"11}

Le format application/problem+json (RFC 9457, ex RFC 7807) donne aux clients une structure stable : type identifie l’erreur, title la résume, status recopie le code, detail explique. Un client peut brancher ses messages d’erreur sur type sans parsing fragile.

#Le contrat requête/réponse, champ par champ

Chaque endpoint définit un contrat dans les deux sens : ce que le client doit envoyer, et ce que le serveur s’engage à renvoyer, y compris quand ça échoue.

Côté requête, quatre éléments à valider avant toute logique métier :

  • La méthode et la route : POST /signup crée, GET /users/:id lit. Une méthode non supportée renvoie 405 Method Not Allowed avec l’en-tête Allow.
  • Les en-têtes de négociation : Content-Type: application/json sinon 415 Unsupported Media Type ; Accept sinon 406 Not Acceptable si vous ne produisez que du JSON.
  • Le corps : JSON syntaxiquement valide sinon 400 Bad Request ; champs présents et sémantiquement corrects sinon 422 Unprocessable Entity avec la liste des champs fautifs.
  • L’authentification et les quotas : 401 si l’identité manque, 403 si elle est connue mais insuffisante, 429 avec Retry-After si le débit est dépassé.

Côté réponse, le code de statut n’est pas décoratif. Ce tableau est le contrat minimal d’une API JSON :

StatutSémantiqueCorps attendu
200 OKLecture ou mise à jour réussieReprésentation de la ressource
201 CreatedCréation réussieRessource créée + Location
204 No ContentSuccès sans corps (souvent DELETE)Aucun
400 Bad RequestRequête malformée (JSON invalide)Problem Details
401 UnauthorizedAuthentification absente ou invalideProblem Details + WWW-Authenticate
403 ForbiddenAuthentifié mais non autoriséProblem Details
404 Not FoundRessource inexistante (ou masquée)Problem Details ou vide
409 ConflictConflit métier (email déjà pris)Problem Details
422 Unprocessable EntityCorps valide mais sémantiquement refuséProblem Details + errors[]
429 Too Many RequestsQuota de débit dépasséProblem Details + Retry-After
500 Internal Server ErrorException non gérée côté serveurProblem Details + traceId

Un serveur fiable gère aussi les délais et la pression. Les timeouts protègent des clients lents ; un limiteur de débit protège des abus ; des logs structurés avec un identifiant de requête simplifient le suivi. La fermeture gracieuse évite de rompre des écritures en vol et limite la corruption de données.

#Animation : vie d’une requête

1
Client
POST /signup (JSON)
2
API
Valider; hacher le mot de passe; créer l’utilisateur
3
DB
INSERT users ; renvoyer id
4
API → Client
201 Created + Set‑Cookie sid sécurisé

#Diagramme : timeouts et arrêt gracieux

Client
Proxy
API
appel async/retour activation fragments
OPT: Timeouts (connect/read)
REGION: Arrêt gracieux
1. POST /signup

#Atelier interactif : cycle requête-réponse

Cycle requête-réponse API
Scénario:
1
Client → API
2
Middleware
3
Validation
4
Contrôleur
5
DB
6
Réponse 201
7
Proxy → Client
Client → APIÉtape 1 / 7
POST /signup (JSON) via proxy/load balancer
Pas de réponse à cette étape.
Vitesse1000ms
Bonnes pratiques: identifiant de requête, erreurs JSON stables, cookies sécurisés, fermeture gracieuse.

Bonnes pratiques à observer durant l’animation :

  • Toujours générer un identifiant de requête et le propager dans les logs et la réponse (X‑Request‑ID).
  • Réponses d’erreur stables en JSON (code machine + message humain + correlationId).
  • Cookies de session sécurisés : HttpOnly, Secure, SameSite=Lax, durée raisonnable.
  • Arrêt gracieux : cesser d’accepter, drainer les connexions, puis quitter.

#Cache HTTP : HIT, MISS, 304

Simulateur de cache HTTP (navigateur/CDN simple)
Politique de réponse
ETag présent
État côté cache
0s
If-None-Match
HITfrais (< max-age=600s)
Règle: no-store → MISS ; frais → HIT ; sinon ETag+If-None-Match → 304 ; à défaut → MISS

Conseils :

  • Préférez ETag + If‑None‑Match pour la revalidation.
  • Cache-Control: no-store désactive le cache (utile pour données sensibles).
  • Un max-age court + revalidation conditionnelle → bonne latence et cohérence.

#Explorateur d’en-têtes HTTP

Explorateur d’en-têtes HTTP (réponse)
Bonnes pratiquesCacheCookiesTraçabilité
Contenu & format
Traçabilité
Cache
Cookie de session
SameSite
Impact
Cacheable (public, max-age=600s)Cookie sécurisé (HttpOnly, Secure, SameSite OK)Traçabilité: X-Request-ID présent
Astuce: pour des APIs, préférez JSON stable et incluez un identifiant de requête. Cookies de session doivent être HttpOnly + Secure, SameSite=Lax/Strict selon le cas.
Aperçu des en-têtes
Content-Type: application/json; charset=utf-8
Cache-Control: public, max-age=600
ETag: W/"abc123"
X-Request-ID: b3b0d2b1c45e
Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=2592000

Conseils pratiques :

  • Réponses API en JSON avec Content-Type: application/json; charset=utf-8.
  • Ajoutez X-Request-ID et exposez-le côté navigateur via Access-Control-Expose-Headers.
  • Cookies de session : HttpOnly; Secure; SameSite=Lax/Strict; Path=/; Max-Age borné.
  • Cache : combinez max-age raisonnable + ETag pour 304 efficaces.

#Limiteur de débit (Token Bucket)

Limiteur de débit — Seau à jetons (Token Bucket)
RPS limiteBurstTrafic
Limite (tokens/s)
20
Capacité (burst)
40
Trafic entrant (req/s)
50
Fenêtre5s
Total250
Admis138 (55.2%)
Bloqués112 (44.8%)
Chronologie (100ms par colonne)
admis
bloqués
Capacité actuelle (tokens) dernière colonne: 0.0
Conseils: dimensionnez limitRPS pour le nominal, ajustez le burst pour absorber des pics courts. Combinez avec files d'attente côté client/proxy, retours 429 avec Retry-After.

Bonnes pratiques :

  • Placez un limiteur au bord (proxy/API gateway) et au niveau de l’application.
  • En dépassement, répondez 429 Too Many Requests et indiquez Retry-After.
  • Combinez avec des files d’attente côté proxy et une montée/descente de trafic progressive.
  • Sur APIs sensibles, limitez par clé client, IP et éventuellement par route.

#Arrêt gracieux (SIGTERM)

Arrêt gracieux (SIGTERM) — simulation
Acceptation ONAucune deadline
Trafic entrant (req/s)
30
Durée moyenne requête (s)
2.5
Deadline d’arrêt (s)
10
In‑flight0
Terminées0
Interrompues0
Rejetées (post‑SIGTERM)0
Total0
Requêtes en cours (barres = temps restant)
Aucune requête en cours
Bonnes pratiques: sur SIGTERM, cessez d’accepter, exposez une deadline raisonnable (env/param), drainez les connexions, et forcez l’arrêt à l’échéance pour éviter des écritures en vol indéfinies. Couplé à un orchestrateur (systemd/K8s) pour rediriger le trafic.

Points clés :

  • Sur SIGTERM : cessez d’accepter, drainez les requêtes en cours, arrêtez à l’échéance.
  • Rendez vos traitements idempotents et transactionnels pour éviter les demi-écritures.
  • Exposez un délai configurable (env/param) cohérent avec l’orchestrateur (systemd/K8s).
  • Couplé au load‑balancer pour retirer le nœud du pool avant l’arrêt.

#CORS : origins, credentials et preflight

Explorateur CORS (Origin, Credentials, Preflight)
Preflight: OKRequête: AUTORISÉE
Client (navigateur)
Origin
Méthode
Content-Type
Serveur (API)
Politique
Allow-Credentials
Allow-Methods
Allow-Headers
Expose-Headers
Preflight (OPTIONS)
Preflight: OK
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, X-Auth-Token
Credentials: include
Réponse préflight
Access-Control-Allow-Methods: OPTIONS, GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, X-Auth-Token
Vary: Origin
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Réponse finale
Requête: AUTORISÉE
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Request-ID
Rappel: avec credentials, ne pas utiliser `Access-Control-Allow-Origin: *`. Faites écho de l’origine autorisée et ajoutez Vary: Origin.
Conseils: préférez listes blanches par origin, exposez les en-têtes utiles via Expose-Headers, et gardez les règles cohérentes côté proxy/API et app. Les en-têtes de requête personnalisés déclenchent un preflight.

Bonnes pratiques :

  • N’utilisez JAMAIS Access-Control-Allow-Origin: * avec des credentials ; faites écho de l’origin autorisé et ajoutez Vary: Origin.
  • Préférez une liste blanche stricte d’origins, ou un wildcard de sous-domaines contrôlé (ex. *.example.com).
  • Exposez uniquement les en-têtes utiles (Access-Control-Expose-Headers: X-Request-ID, ETag, …).
  • Un preflight est requis si la requête n’est pas « simple » (méthodes non simples, en-têtes personnalisés, Content-Type non simple).

#Retry et backoff exponentiel avec jitter

Retry + backoff exponentiel avec jitter
Succès à l’essai #3 (406ms)Attempts: 3Temps total: 406ms
Délais
Base (ms)200
Facteur2.0
Max (ms)4000
Stratégie
Max essais6
Jitter
Jitter recommandé: full ou decorrelated, pour éviter la synchronisation de retries (thundering herd).
Succès probabiliste
P(succès)40%
Seed42
Modélise un service flaky: chaque essai a une chance de réussite indépendante.
Chronologie des essais
#1
#2
#3
Barres verticales: instants des tentatives. Vert = succès; Rouge = échec. Les délais sont calculés selon la stratégie sélectionnée.
Bonnes pratiques: limiter le nombre d’essais, utiliser jitter, arrêter après erreurs non-retryables (4xx), et propager un idempotency-key si nécessaire.

Conseils :

  • Limitez le nombre d’essais et appliquez un backoff exponentiel avec jitter (full ou decorrelated).
  • Arrêtez les retries sur erreurs non-retryables (4xx), et utilisez des idempotency-key pour éviter les doublons.
  • Combinez avec un limiteur et un circuit-breaker en amont pour protéger le service.

#Exemples de code pratiques

#Middleware d’erreurs (Problem Details) + corrélation

tsts

1import type { Request, Response, NextFunction } from 'express'2import { randomUUID } from 'node:crypto'3import pino from 'pino'4 5export const logger = pino({ level: process.env.LOG_LEVEL || 'info' })6 7export function withRequestId(req: Request, res: Response, next: NextFunction) {8  const traceId = req.get('x-request-id') || randomUUID()9  res.locals.traceId = traceId10  res.setHeader('X-Request-ID', traceId)11  ;(req as any).logger = logger.child({ traceId })12  next()13}14 

#Validation du corps : 400, 422 et erreurs par champ

tsts

1import { z } from 'zod'2 3const SignupSchema = z.object({4  email: z.string().email(),5  password: z.string().min(8, 'au moins 8 caractères'),6})7 8app.post('/signup', async (req, res) => {9  const parsed = SignupSchema.safeParse(req.body)10  if (!parsed.success) {11    // 422: le JSON est valide mais les données ne respectent pas le contrat12    const errors = parsed.error.issues.map((i) => ({ field: i.path.join('.'), message: i.message }))13    return res.status(422).json({14      type: 'https://example.com/errors/validation',

Le JSON malformé est intercepté plus tôt : le parseur express.json() déclenche une erreur de syntaxe que le middleware d’erreurs convertit en 400 Bad Request. La frontière est nette : 400 pour la syntaxe, 422 pour la sémantique.

#Timeouts côté Node/Express

tsts

1import http from 'node:http'2const server = http.createServer(app)3 4// Évitez les clients lents : timeouts raisonnables5server.headersTimeout = 5_000       // délai pour recevoir les en-têtes6server.keepAliveTimeout = 5_000     // durée d’inactivité sur connexions keep-alive7;(server as any).requestTimeout = 10_000 // (Node >=18) délai total d’une requête8 9// Timeouts côté client (fetch/undici) avec AbortController10async function getWithTimeout(url: string, ms = 5000) {11  const ac = new AbortController()12  const t = setTimeout(() => ac.abort(), ms)13  try {14    const res = await fetch(url, { signal: ac.signal })
Solution

Proxies et timeouts Définissez aussi des timeouts côté proxy (connect/read) et retournez des erreurs JSON stables. Harmonisez les délais (proxy < serveur < client) pour éviter des requêtes zombi.

#Limiteur de débit (token bucket simple)

tsts

1type Bucket = { tokens: number; last: number }2const buckets = new Map<string, Bucket>()3 4export function rateLimit({ rate = 5, perMs = 1000, burst = 10 } = {}) {5  return (req, res, next) => {6    const key = req.ip // En prod: combinez clé API, IP, route7    const now = Date.now()8    const b: Bucket = buckets.get(key) || { tokens: burst, last: now }9    // Refill10    const refill = ((now - b.last) / perMs) * rate11    b.tokens = Math.min(burst, b.tokens + refill)12    b.last = now13    if (b.tokens < 1) {14      res.setHeader('Retry-After', '1')

#Arrêt gracieux (SIGTERM)

tsts

1import type { Socket } from 'node:net'2 3const PORT = process.env.PORT ? Number(process.env.PORT) : 30004const server = app.listen(PORT, () => logger.info({ PORT }, 'listening'))5 6const sockets = new Set<Socket>()7server.on('connection', (s: Socket) => {8  sockets.add(s)9  s.on('close', () => sockets.delete(s))10})11 12let shuttingDown = false13process.on('SIGTERM', () => {14  if (shuttingDown) return

#Endpoints de santé

tsts

1app.get('/healthz', (_req, res) => res.status(200).json({ ok: true }))2app.get('/readyz', async (_req, res) => {3  try {4    await db.query('SELECT 1')5    res.json({ ready: true })6  } catch {7    res.status(503).json({ ready: false })8  }9})

/healthz répond « le processus vit » pour les orchestrateurs ; /readyz vérifie les dépendances (base, cache) avant de recevoir du trafic. Un pod qui ne passe pas readyz est sorti du pool sans être tué : c’est la sonde qui distingue « démarré » et « utile ».

#Mini-exercice : boucler le contrat d’erreurs

Partez du endpoint POST /signup ci-dessus et complétez-le :

  1. Ajoutez un middleware d’erreurs qui transforme toute exception en Problem Details {type, title, status, detail, traceId} et journalise l’exception avec le même traceId.
  2. Renvoyez 409 avec type: "https://example.com/errors/email-taken" quand l’email existe déjà, sans faire fuiter le hash du mot de passe dans la réponse.
  3. Ajoutez l’arrêt gracieux (SIGTERM) qui termine les connexions en cours avant de quitter.
Correction guidée
  1. Middleware d’erreurs : montez withRequestId avant les routes pour peupler res.locals.traceId, puis errorHandler en dernier. Dans le handler, loggez req.logger.error({ err }, 'unhandled_error') et répondez via problem(res, status, title). En production, retirez err.message du corps pour les statuts 500 : le détail interne va dans les logs, pas au client.
  2. Conflit email : interceptez l’erreur unique de la base (code 23505 sous PostgreSQL) et transformez-la en 409 avec le type documenté. Ne renvoyez jamais l’objet utilisateur complet : seulement { id }.
  3. Arrêt gracieux : sur SIGTERM, server.close(), fin polie des sockets, deadline de sortie forcée à 30 s, et exit(0) dans le callback. Vérifiez avec kill -TERM <pid> qu’une requête en vol se termine avant la sortie.

#Quiz rapide

Après une création réussie d’utilisateur avec session, quel statut et format recommandez-vous ?
Après une création réussie d’utilisateur avec session, quel statut et format recommandez-vous ?
Le client envoie un JSON syntaxiquement valide mais un champ password de 3 caractères. Quelle réponse ?
Le client envoie un JSON syntaxiquement valide mais un champ password de 3 caractères. Quelle réponse ?