HTTP et serveurs
Progression
#HTTP et serveurs
Exposez une API claire: des routes explicites, des statuts cohérents (200/201/204/400/401/403/404/409/422/500) 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.
Exemple en pseudo‑code:
1app.post('/signup', validate(SignupSchema), async (req, res) => {2 const { email, password } = req.body3 const hash = await hashPassword(password)4 const user = await db.users.insert({ email, password_hash: hash })5 const session = await createSession(user.id)6 res.cookie('sid', session.id, { httpOnly: true, secure: true, sameSite: 'Lax' })7 res.status(201).json({ id: user.id })8})
Mini‑exercice: ajoutez un middleware d’erreurs qui transforme toute exception en JSON {error: code, message, correlationId}
et journalise avec le même correlationId
. Ajoutez un arrêt gracieux du serveur (SIGTERM) qui termine les connexions en cours avant de quitter.
Un serveur fiable gère 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-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
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 viaAccess-Control-Expose-Headers
. - Cookies de session:
HttpOnly; Secure; SameSite=Lax/Strict; Path=/; Max-Age=...
. - Cache: combinez
max-age
raisonnable + 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 Requests
et 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-Type
non 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-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
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
Montez d’abord les middlewares techniques (request id, parseurs), puis vos routes, et enfin le error handler pour capter toutes les erreurs.
#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 })
#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')
Ne faites confiance à X‑Forwarded‑For
que si votre serveur connaît les proxies en amont (app.set('trust proxy', true)
) et qu’ils sont sous votre contrôle.
#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})