IFT3225

Le problème : HTTP ne se souvient de rien

Le protocole HTTP est sans état (stateless). Chaque requête est traitée de manière indépendante : le serveur n’est pas tenu de garder une trace des requêtes et réponses précédentes. C’est une force pour la mise à l’échelle, mais en pratique une requête est souvent liée à une autre ou requiert un contexte pour être bien traitée. D’où la question: Où préserver l’information nécessaire pour un traitement efficace des requêtes ?

C’est tout l’enjeu de la persistance. Et il n’y a pas une seule réponse : on peut préserver l’information à plusieurs endroits — sur le client, en mémoire du serveur, dans une base de données, dans un cache et même au niveau de l’utilisateur ou l’environnement (laissé en dehors de ce guide) — chacun avec ses forces et son coût.

Du flux sans persistance aux mécanismes de préservation

Le meilleur moyen de comprendre chaque mécanisme, c’est de partir de l’état de base (sans aucune persistance) et d’observer ce qu’on est obligé de faire : tout transporter dans chaque requête. Puis on ajoute les mécanismes un par un. À chaque ajout, la checklist de ce qu’il faut renvoyer rétrécit, mais l’architecture gagne en complexité.

L’explorateur ci-dessous trace graphiquement l’échange sous forme de diagramme de séquence animé.
Pour l’utiliser,

  1. Commencez par choisir un type de client :
    • un navigateur ou une application mobile utilisé par Alice pour emprunter un livre et consulter son historique ;
    • un objet connecté (le capteur d’une boîte) qui pousse un événement puis interroge sa configuration. Un scénario où le cache prend tout son sens.
  2. Cochez ensuite les mécanismes de persistance (cases indépendantes) : Client, Serveur, Base de données, Cache
  3. Pour chacun, choisissez la technologie . Le flux se redessine automatiquement.
  4. Consultez sous le diagramme les considérations pour la technologie sélectionnée: ce dont elle a besoin, ce qu’on peut y stocker, comment la contrôler, ses limites et quand l’utiliser.

Scénario. Alice consulte une boîte Lino, emprunte un livre, puis consulte son historique. Le type de client change le scénario et les mécanismes disponibles.

Type de client
Navigateur
Serveur

Icône () = nature du payload | Barres () = taille (survolez pour le détail).

Chaque mécanisme déplace une responsabilité — vers le client, le serveur, la BD — et chaque déplacement a un prix. Il n’existe pas de « meilleur » endroit dans l’absolu : il y a des endroits adaptés à un type de donnée et à un type d’accès.

Les cibles de persistance

Persister une donnée ne veut pas toujours dire la conserver dans une base de données. L’état peut être conservé à plusieurs endroits : dans le navigateur, dans la mémoire du serveur, dans une base de données, dans un cache, ou même dans un réseau de diffusion comme un CDN.

Côté client

Le client peut mémoriser de l’information localement. Cette information peut servir à identifier une session, personnaliser l’expérience, conserver un état d’interface ou permettre un fonctionnement partiel hors-ligne.

Cependant tout stockage côté client doit être traité avec prudence : l’utilisateur peut l’effacer et le navigateur peut le limiter. On ne doit donc jamais considérer le client comme une source de vérité fiable pour des données critiques.

Cookies (navigateur)

Un cookie est une petite paire nom=valeur, typiquement limitée à environ 4 Ko, que le navigateur conserve et associe à un domaine.

Dans les usages serveur classiques, c’est le serveur qui crée le cookie en ajoutant un en-tête Set-Cookie à sa réponse. Le JavaScript de la page peut aussi créer certains cookies avec document.cookie, mais on évite cette approche pour les données sensibles.

Ensuite, à chaque requête vers le domaine concerné, le navigateur renvoie automatiquement le cookie dans l’en-tête Cookie. Le code client n’a pas à l’ajouter manuellement. Le serveur peut alors relire ce cookie pour reconnaître le même client d’une requête à l’autre.

// Express — le serveur pose le cookie dans sa réponse
res.cookie("sid", id, {
  httpOnly: true,
  secure: true,
  sameSite: "lax",
  maxAge: 600000
});

// Requêtes suivantes — le serveur le relit
const sid = req.cookies.sid;

Les attributs du cookie contrôlent sa portée, sa durée de vie et certains aspects de sécurité :

Comme un cookie est renvoyé automatiquement avec les requêtes HTTP correspondantes, on évite d’y stocker des données volumineuses. En pratique, on y met surtout un petit identifiant, par exemple un identifiant de session. L’état complet reste ailleurs, souvent côté serveur.

Un cookie ne devrait jamais contenir de mot de passe, de secret en clair, ou de données personnelles détaillées.

Exemples d’usage:

Web Storage : localStorage et sessionStorage

Le Web Storage est un espace de stockage clé-valeur tenu par le navigateur et propre à une origine. Il offre généralement plus d’espace qu’un cookie, souvent autour de 5 à 10 Mo selon le navigateur.

Contrairement aux cookies, le contenu du Web Storage n’est jamais envoyé automatiquement au serveur. Il s’agit d’un stockage local, pas d’un mécanisme de transport HTTP.

C’est le JavaScript côté client qui écrit et lit explicitement les données avec setItem et getItem.

localStorage.setItem("brouillon", JSON.stringify(donnees));

const donnees = JSON.parse(
  localStorage.getItem("brouillon") ?? "null"
);

Pour qu’une donnée stockée dans localStorage atteigne le serveur, le JavaScript doit la relire et l’inclure explicitement dans une requête, par exemple dans le corps d’un fetch.

La différence entre localStorage et sessionStorage concerne principalement la durée de vie :

Le Web Storage offre plus d’espace qu’un cookie, mais il reste limité et synchrone. Surtout, il est accessible au JavaScript : il ne faut donc pas y stocker de secret, de mot de passe ou de jeton sensible exposé aux attaques XSS.

Exemples d’usage:

Côté serveur (en mémoire)

Le serveur peut aussi conserver de l’information dans sa propre mémoire vive. Cette solution est très rapide, mais fragile : la mémoire appartient à un processus particulier. Elle disparaît au redémarrage et n’est pas automatiquement partagée entre plusieurs serveurs.

Elle convient bien à un état temporaire ou non critique, mais elle ne doit pas être utilisée seule pour des données que l’on doit conserver durablement.

Structure en mémoire : Map

Une structure comme Map ou un simple objet JavaScript peut servir de magasin temporaire dans un serveur Node.js.

C’est le code serveur qui la crée, généralement au démarrage de l’application, puis qui y écrit et relit au fil des requêtes.

const sessions = new Map();

sessions.set(sid, { emprunts: [] }); // une requête écrit
const etat = sessions.get(sid);      // une autre requête relit

Cette mémoire est partagée entre toutes les requêtes traitées par le même processus. Une requête peut donc écrire une valeur, et une autre requête peut la retrouver plus tard avec la même clé.

Cependant, cette donnée ne voyage pas sur le réseau, n’est pas partagée entre plusieurs instances du serveur, et disparaît si le processus redémarre.

Exemples d’usage :

La base de données

La base de données correspond à la persistance durable, partagée et interrogeable.

Contrairement à la mémoire serveur, les données survivent aux redémarrages, et contrairement au stockage client, elles ne dépendent pas du navigateur de l’utilisateur.

Dans une application web classique, c’est le serveur applicatif qui lit et écrit dans la base à l’aide d’un pilote, d’un ORM ou d’un ODM. Chaque accès à la base implique généralement une requête réseau séparée vers un service externe, parfois situé sur une autre machine.

C’est pourquoi le code d’accès aux données est souvent asynchrone.

const emprunt = await Emprunt.create({
  emprunteur,
  livreId,
  date: new Date()
});

La base de données n’est pas simplement un endroit où déposer des données. Elle joue souvent le rôle de source de vérité applicative : c’est l’état durable sur lequel le système s’appuie pour reconstruire ses réponses, ses écrans, ses rapports et ses décisions.

BD NoSQL (orientée document)

Une BD NoSQL comme MongoDB stocke les données sous forme de documents BSON. Ces documents ressemblent à des objets JSON enrichis et peuvent contenir des tableaux, des objets imbriqués et des champs variables.

Avec Mongoose, le serveur manipule ces documents à travers un ODM qui fournit des modèles, des schémas applicatifs et des méthodes de requête.

const emprunt = await Emprunt.create({
  emprunteur,
  livreId,
  date: new Date()
});

const historique = await Emprunt
  .find({ emprunteur })
  .sort({ date: -1 });

Le modèle orientée document est particulièrement adapté lorsque les données sont souvent lues en bloc, lorsqu’elles ont une structure naturellement imbriquée, ou lorsque le schéma peut évoluer au fil du temps.

Avec MongoDB, on doit souvent choisir entre deux stratégies de modélisation :

L’imbrication facilite la lecture en bloc, mais peut créer de la duplication. La référence réduit la duplication, mais oblige parfois à faire plusieurs requêtes ou des agrégations.


BD relationnelle

Une BD relationnelle comme PostgreSQL organise les données en tables à schéma fixe. Elle offre des contraintes fortes, des jointures, des transactions ACID et un langage de requête standardisé : SQL.

INSERT INTO emprunts (emprunteur, livre_id)
VALUES ($1, $2)
RETURNING *;

SELECT *
FROM emprunts
WHERE emprunteur = $1
ORDER BY date DESC;

Le modèle relationnel est particulièrement adapté lorsque les données sont fortement liées, lorsque les contraintes d’intégrité sont importantes, ou lorsqu’une opération doit modifier plusieurs tables de façon atomique.

Une transaction permet de regrouper plusieurs opérations et de garantir qu’elles réussissent ou échouent ensemble.

BEGIN;

INSERT INTO emprunts (emprunteur, livre_id)
VALUES ($1, $2);

UPDATE livres
SET disponible = false
WHERE id = $2;

COMMIT;

Si une des opérations échoue, la transaction peut être annulée. Cela évite de laisser le système dans un état partiellement modifié.

Le cache et les autres

Un cache ne joue pas le même rôle qu’une base de données. Il ne sert pas d’abord à conserver la vérité durable du système, mais à éviter de refaire un travail coûteux.

Un cache conserve une copie : copie d’une lecture fréquente, d’un calcul coûteux, d’une réponse HTTP, ou d’un état temporaire. Cependant, cette copie peut devenir périmée. Le problème central du cache est donc l’invalidation : quand faut-il supprimer ou rafraîchir la copie?

Redis côté serveur

Redis est un magasin clé-valeur en mémoire, généralement exécuté comme un service séparé du serveur applicatif. Il est très rapide et peut associer une durée de vie aux clés avec un TTL.

Contrairement à une Map dans Node, Redis peut être partagé par plusieurs instances du serveur. Il peut donc servir de cache commun, de store de session partagé, ou de compteur distribué.

await redis.set(
  "boite:42",
  JSON.stringify(boite),
  "EX",
  60
);

const cache = await redis.get("boite:42");

Un patron courant est le cache-aside :

  1. le serveur reçoit une requête et cherche d’abord la donnée dans Redis;
  2. si elle existe, c’est un cache hit;
  3. si elle n’existe pas, c’est un cache miss;
  4. le serveur interroge alors la base de données;
  5. il recopie le résultat dans Redis avec un TTL;
  6. les prochaines requêtes pourront utiliser le cache.

Le TTL limite la durée de vie de la copie. Mais il ne règle pas tout : si la donnée change dans la base, il faut parfois invalider explicitement la clé Redis pour éviter de servir une version périmée.

Exemples d’usage :


Content Delivery Network (CDN)

Un CDN, ou réseau de diffusion de contenu, conserve des copies de certaines réponses HTTP sur des serveurs situés près des utilisateurs.

Contrairement à Redis, le CDN n’est pas utilisé directement par le code applicatif comme une base clé-valeur. Il intercepte plutôt des requêtes HTTP et peut répondre à la place du serveur d’origine lorsque la ressource est cacheable.

Le serveur contrôle le comportement du CDN au moyen d’en-têtes HTTP, notamment Cache-Control.

Cache-Control: public, max-age=300

Avec cet en-tête, une réponse publique peut être conservée pendant 300 secondes. Pendant ce temps, des requêtes identiques peuvent être servies par le CDN. Cela réduit la latence pour l’utilisateur et diminue la charge sur le serveur applicatif.

Un CDN convient aux ressources publiques ou peu variables. Il ne convient pas aux données privées ou fortement personnalisées, sauf avec une configuration très précise.

Exemples d’usage :

En un coup d’œil

Chaque stratégie de persistance a un rôle architectural distinct. Le bon choix dépend moins de la technologie elle-même que de la nature de la donnée et du type d’état que l’on veut conserver.

Famille Survit au redémarrage? Partagée entre serveurs? Rôle architectural Usage de prédilection
Cookie Selon Max-Age / Expires Non, propre au client Identifiant ou petite préférence envoyée avec HTTP Session, langue, thème
Web Storage Oui pour localStorage, non durable pour sessionStorage Non, propre à l’appareil et à l’origine État local côté client Brouillon, préférences, cache local
Mémoire serveur Non Non, sauf instance unique État temporaire d’une instance Cache court, compteur, état non critique
Base de données Oui Oui Source durable et interrogeable Données métier, historique, utilisateurs
Cache Redis Optionnel selon configuration Oui Cache, session partagée, compteur distribué Lectures fréquentes, rate limiting, sessions
Cache CDN Jusqu’au TTL ou à la purge Oui, à l’échelle du réseau Copie publique proche de l’utilisateur Images, JS, CSS, pages publiques

Comment choisir

En pratique, une application combine plusieurs familles : un cookie pour la session, une base pour le durable, un cache devant les lectures chaudes. Le but n’est pas d’en empiler le plus, mais de choisir le minimum qui répond aux besoins. Pour choisir une cible de persistance, on peut utiliser les règles suivantes :

Le choix d’une stratégie de persistance est donc un choix d’architecture : il détermine où se trouve l’état, qui lui fait confiance, et ce qui arrive lorsque le navigateur, le serveur, le réseau ou la base tombe en panne.
VérificationPour Lino : où ranger (a) l'identifiant de session d'Alice (sid), (b) l'historique de ses emprunts ? Justifiez.

(a) Le sid vit côté client (un cookie) : il accompagne chaque requête pour que le serveur retrouve la bonne session, sans avoir à s’en souvenir de lui-même. (b) L’historique des emprunts doit survivre et se consulter → base de données (durable, interrogeable) ; il serait perdu s’il ne vivait qu’en mémoire serveur, et invisible aux autres appareils s’il ne vivait qu’en localStorage. Chaque donnée va là où son besoin (identité de session, durabilité) est le mieux servi.