Backend pratique · L2 · Section 1/6
HTTP et serveurs
Progression
#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 :
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"}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 :
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 /signupcrée,GET /users/:idlit. Une méthode non supportée renvoie405 Method Not Allowedavec l’en-têteAllow. - Les en-têtes de négociation :
Content-Type: application/jsonsinon415 Unsupported Media Type;Acceptsinon406 Not Acceptablesi vous ne produisez que du JSON. - Le corps : JSON syntaxiquement valide sinon
400 Bad Request; champs présents et sémantiquement corrects sinon422 Unprocessable Entityavec la liste des champs fautifs. - L’authentification et les quotas :
401si l’identité manque,403si elle est connue mais insuffisante,429avecRetry-Aftersi 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 :
| Statut | Sémantique | Corps attendu |
|---|---|---|
| 200 OK | Lecture ou mise à jour réussie | Représentation de la ressource |
| 201 Created | Création réussie | Ressource créée + Location |
| 204 No Content | Succès sans corps (souvent DELETE) | Aucun |
| 400 Bad Request | Requête malformée (JSON invalide) | Problem Details |
| 401 Unauthorized | Authentification absente ou invalide | Problem Details + WWW-Authenticate |
| 403 Forbidden | Authentifié mais non autorisé | Problem Details |
| 404 Not Found | Ressource inexistante (ou masquée) | Problem Details ou vide |
| 409 Conflict | Conflit métier (email déjà pris) | Problem Details |
| 422 Unprocessable Entity | Corps valide mais sémantiquement refusé | Problem Details + errors[] |
| 429 Too Many Requests | Quota de débit dépassé | Problem Details + Retry-After |
| 500 Internal Server Error | Exception non gérée côté serveur | Problem 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
#Diagramme : timeouts et arrêt gracieux
#Atelier interactif : cycle requête-réponse
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
Conseils :
- Préférez ETag + If‑None‑Match pour la revalidation.
Cache-Control: no-storedésactive le cache (utile pour données sensibles).- Un
max-agecourt + revalidation conditionnelle → bonne latence et cohérence.
#Explorateur d’en-têtes HTTP
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-IDet exposez-le côté navigateur viaAccess-Control-Expose-Headers. - Cookies de session :
HttpOnly; Secure; SameSite=Lax/Strict; Path=/; Max-Ageborné. - Cache : combinez
max-ageraisonnable + ETag pour 304 efficaces.
#Limiteur de débit (Token Bucket)
Bonnes pratiques :
- Placez un limiteur au bord (proxy/API gateway) et au niveau de l’application.
- En dépassement, répondez
429 Too Many Requestset indiquezRetry-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)
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
Origin: https://app.example.com Access-Control-Request-Method: POST Access-Control-Request-Headers: Content-Type, X-Auth-Token Credentials: include
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
Access-Control-Allow-Origin: https://app.example.com Vary: Origin Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: X-Request-ID
Bonnes pratiques :
- N’utilisez JAMAIS
Access-Control-Allow-Origin: *avec des credentials ; faites écho de l’origin autorisé et ajoutezVary: 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-Typenon simple).
#Retry et backoff exponentiel avec jitter
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-keypour é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
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
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
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)
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)
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é
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 :
- Ajoutez un middleware d’erreurs qui transforme toute exception en Problem Details
{type, title, status, detail, traceId}et journalise l’exception avec le mêmetraceId. - Renvoyez
409avectype: "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. - Ajoutez l’arrêt gracieux (SIGTERM) qui termine les connexions en cours avant de quitter.
Correction guidée
- Middleware d’erreurs : montez
withRequestIdavant les routes pour peuplerres.locals.traceId, puiserrorHandleren dernier. Dans le handler, loggezreq.logger.error({ err }, 'unhandled_error')et répondez viaproblem(res, status, title). En production, retirezerr.messagedu corps pour les statuts 500 : le détail interne va dans les logs, pas au client. - Conflit email : interceptez l’erreur unique de la base (code
23505sous PostgreSQL) et transformez-la en409avec letypedocumenté. Ne renvoyez jamais l’objet utilisateur complet : seulement{ id }. - Arrêt gracieux : sur SIGTERM,
server.close(), fin polie des sockets, deadline de sortie forcée à 30 s, etexit(0)dans le callback. Vérifiez aveckill -TERM <pid>qu’une requête en vol se termine avant la sortie.