Votre API envoie probablement le même JSON des milliers de fois par jour. Un client demande GET /v1/products/42, reçoit 18 Ko, redemande cinq minutes plus tard, et reçoit les mêmes 18 Ko. Rien n'a changé. Vous avez payé pour la bande passante, la sérialisation et la lecture de la base de données de toute façon.
HTTP a déjà résolu ce problème. L'en-tête Cache-Control indique aux clients combien de temps une réponse reste fraîche. L'en-tête ETag leur donne une empreinte numérique pour vérifier si elle a changé. Ensemble, ils transforment les requêtes répétées en réponses 304 Not Modified avec des corps vides, et ils peuvent en prime protéger vos écritures contre les mises à jour perdues. Les mêmes idées animent également les modèles côté client ; si vous avez lu notre guide sur la mise en cache des réponses API dans React, ceci est la partie serveur de cette histoire.
Ce guide passe en revue les trois couches de la mise en cache HTTP, montre le cycle complet du 304 étape par étape, démêle `no-cache` et `no-store`, et se termine par un code Express fonctionnel. Vous verrez également comment vérifier tout cela dans Apidog en envoyant des en-têtes conditionnels et en affirmant vous-même le 304.
Les trois couches de la mise en cache HTTP
La mise en cache HTTP pour les API se décompose en trois décisions distinctes. Les équipes rencontrent des problèmes lorsqu'elles les mélangent.
Couche 1 : Fraîcheur. Combien de temps un client peut-il réutiliser une réponse sans vous demander du tout ? C'est Cache-Control: max-age=60. Pendant 60 secondes, le client utilise la copie mise en cache localement. Zéro trafic réseau. C'est le cache hit le moins cher possible et aussi le plus risqué, car le client ne peut pas détecter un changement tant que le délai n'est pas expiré.
Couche 2 : Validation. Une fois que la réponse est périmée, le client n'a pas besoin de la télécharger à nouveau. Il demande "cela a-t-il changé ?" en envoyant l'empreinte numérique que vous lui avez donnée précédemment. Si la ressource est inchangée, vous répondez par un 304 Not Modified sans corps. L'ETag avec If-None-Match est la version précise de cela ; Last-Modified avec If-Modified-Since est la version plus ancienne, basée sur l'horodatage, avec une granularité d'une seconde.
Couche 3 : Invalidation. Quand les données changent, comment les copies périmées disparaissent-elles ? Les caches clients privés expirent d'eux-mêmes via `max-age`. Les caches partagés et les CDN nécessitent des purges explicites, des TTL courts, ou des directives comme `stale-while-revalidate` qui limitent la désuétude.
La fraîcheur économise le plus, la validation rattrape tout ce que la fraîcheur manque, et l'invalidation maintient l'intégrité des deux. La plupart des API ont besoin des trois.
Comment fonctionne un cycle de requête/réponse 304 Not Modified
Voici le cycle complet pour un point de terminaison de produit, étape par étape.
Première requête. Le client n'a rien mis en cache :
GET /v1/products/42 HTTP/1.1
Host: api.example.com
Première réponse. Vous retournez le corps plus les métadonnées de mise en cache :
HTTP/1.1 200 OK
Cache-Control: private, max-age=60
ETag: "33a64df551425fcc55e4d42a148795d9f2"
Content-Type: application/json
Content-Length: 18432
Le client stocke le corps et l'ETag. Pendant les 60 secondes suivantes, il ne vous contacte pas du tout.
Deuxième requête, après 60 secondes. La copie est périmée, le client la revalide donc :
GET /v1/products/42 HTTP/1.1
Host: api.example.com
If-None-Match: "33a64df551425fcc55e4d42a148795d9f2"
Deuxième réponse, ressource inchangée. Votre serveur compare l'ETag entrant avec l'ETag actuel. Ils correspondent, donc :
HTTP/1.1 304 Not Modified
Cache-Control: private, max-age=60
ETag: "33a64df551425fcc55e4d42a148795d9f2"
Pas de corps. Au lieu de 18 Ko, la réponse est de quelques centaines d'octets d'en-têtes. Le client marque sa copie mise en cache comme fraîche pour 60 secondes supplémentaires et la sert. Si le produit avait changé, vous retourneriez un 200 normal avec le nouveau corps et un nouvel ETag. Nous avons couvert le code de statut lui-même plus en détail dans notre explication du 304 Not Modified ; la version courte est qu'un 304 est une instruction de cache, pas une erreur.
Les avantages économiques sont simples. Une requête GET conditionnelle coûte toujours un aller-retour plus le travail nécessaire pour calculer l'ETag actuel. Ce qu'elle élimine, c'est le transfert de la charge utile et la ré-analyse côté client. Pour les points de terminaison de listes volumineuses interrogés par des clients mobiles, cela réduit régulièrement l'égression d'API de 60 à 90 pour cent.
Directives Cache-Control importantes pour les API
Cache-Control contient plus d'une douzaine de directives. Pour les API JSON, cinq d'entre elles portent la majeure partie du poids.
no-store vs no-cache. C'est le bug de cache le plus courant dans les API de production, et il se manifeste dans les deux sens. no-store signifie "ne jamais écrire ceci dans aucun cache". Utilisez-le pour des charges utiles véritablement sensibles : jetons, données bancaires, PII que vous ne devez pas persister. no-cache signifie presque le contraire de ce que cela semble : les caches PEUVENT stocker la réponse, mais ils doivent revalider avec l'origine avant chaque réutilisation. Associé à un ETag, no-cache vous permet de réaliser des économies de 304 sur chaque requête tout en garantissant que les clients n'affichent jamais de données périmées. Les équipes qui appliquent no-store à tout "pour être sûr" désactivent complètement les requêtes conditionnelles et paient le coût total de la charge utile à chaque appel.
private. Marque la réponse comme pouvant être mise en cache uniquement par le client de l'utilisateur final, jamais par les caches partagés ou les CDN. Toute réponse qui varie par utilisateur, ce qui est le cas de la plupart du trafic API authentifié, devrait porter la directive private. Sans elle, un proxy mal configuré peut servir les données de compte d'un utilisateur à un autre.
max-age. Durée de vie de la fraîcheur en secondes. Pour les API, pensez petit : 30 à 300 secondes couvrent la plupart des points de terminaison de lecture. Vous n'essayez pas d'éliminer les requêtes pendant une journée ; vous essayez d'absorber les pics et les boucles de sondage.
stale-while-revalidate. Le juste milieu pragmatique. Cache-Control: max-age=60, stale-while-revalidate=300 indique aux caches : servez la copie périmée pendant 5 minutes supplémentaires, mais rafraîchissez-la en arrière-plan. Les utilisateurs obtiennent des réponses instantanées ; votre origine est mise à jour peu de temps après. Les CDN comme Cloudflare et Fastly le prennent en charge, tout comme les navigateurs.
Un défaut sensé pour un point de terminaison de lecture authentifié ressemble à ceci :
Cache-Control: private, max-age=60, stale-while-revalidate=120
ETag: "9f8b2c41aa73e0d5"
La spécification comportementale complète se trouve dans la RFC 9111, qui a remplacé la RFC 7234 comme document définitif sur la mise en cache HTTP. Lorsqu'un CDN se comporte d'une manière qui vous surprend, c'est dans cette RFC que se trouve la réponse.
ETags forts vs faibles
Un ETag existe en deux variantes, et le préfixe W/ les sépare.
Un ETag fort (ETag: "33a64df551425fcc") promet une égalité octet par octet. Deux réponses avec le même ETag fort sont identiques, ce qui rend les ETags forts sûrs pour les requêtes de plage d'octets et requis pour le contrôle de concurrence avec If-Match.
Un ETag faible (ETag: W/"33a64df551425fcc") promet une équivalence sémantique. Les octets peuvent différer, peut-être l'ordre des champs a-t-il changé ou un champ d'horodatage a-t-il avancé, mais la signification est la même, de sorte qu'un cache peut conserver sa copie.
Là où cela vous pose problème : le middleware de compression. Nginx et certains frameworks réécrivent les ETags forts en ETags faibles lorsqu'ils compressent une réponse à la volée, car les octets compressés ne correspondent plus à l'original. Si vos vérifications de concurrence échouent mystérieusement derrière un proxy, recherchez un préfixe W/ qui n'était pas présent lorsque votre serveur d'application a envoyé la réponse.
Par défaut, utilisez des ETags forts calculés sur le corps non compressé. N'utilisez des ETags faibles que lorsque vous servez sciemment des représentations variantes des mêmes données.
Génération d'ETags : hachage du corps vs colonne de version
Deux stratégies dominent, et la bonne dépend de l'endroit où réside le coût.
Hachage du corps de la réponse. Sérialisez la réponse, hachez-la (MD5 ou SHA-1 est suffisant ici ; c'est une empreinte numérique, pas une limite de sécurité), et citez-la. C'est précis par construction et ne nécessite aucune modification de schéma. L'inconvénient : vous construisez la réponse complète à chaque requête, y compris les 304. Vous économisez de la bande passante mais pas la puissance de calcul ou la charge de la base de données.
Colonne de version ou updated_at. Dérivez l'ETag à partir de données que vous pouvez récupérer à moindre coût : ETag: "42-v17" à partir du compteur de version de la ligne, ou un hachage de updated_at. Maintenant, une requête conditionnelle coûte une recherche indexée au lieu d'une sérialisation complète. L'inconvénient : la version doit être incrémentée à chaque changement qui affecte la réponse, y compris les changements dans les tables jointes. En manquer un et vous servirez des 304 périmées, ce qui est le pire bug de cache car il est invisible.
Commencez par le hachage du corps. C'est correct par défaut. Déplacez les points de terminaison très sollicités vers des ETags basés sur la version lorsque le profilage montre que le coût de la sérialisation est important.
ETags pour la concurrence optimiste : If-Match et 412
La même empreinte numérique qui économise la bande passante lors des lectures empêche les mises à jour perdues lors des écritures.
Le problème de la mise à jour perdue : deux administrateurs chargent le produit 42 en même temps. L'administrateur A modifie le prix et enregistre. L'administrateur B corrige une faute de frappe et enregistre 30 secondes plus tard, écrasant le changement de prix de A avec le prix périmé que B a chargé. Personne ne voit d'erreur. Les données sont silencieusement incorrectes.
La solution consiste à rendre chaque mise à jour conditionnelle à la version que le client a vue pour la dernière fois :
PUT /v1/products/42 HTTP/1.1
If-Match: "33a64df551425fcc55e4d42a148795d9f2"
Content-Type: application/json
Le serveur compare If-Match avec l'ETag actuel de la ressource. S'il y a correspondance : appliquez la mise à jour, retournez 200 avec un nouvel ETag. Pas de correspondance, quelqu'un d'autre est arrivé le premier : rejetez avec 412 Precondition Failed et ne touchez pas aux données. Le client re-récupère alors, réapplique son changement sur la version fraîche, et réessaie. Les API strictes vont plus loin et retournent 428 Precondition Required sur toute requête PUT qui omet If-Match, rendant la vérification de sécurité obligatoire.
Cela ne vous coûte presque rien à ajouter une fois que les ETags existent, et cela transforme un bug de corruption de données silencieux en un statut HTTP explicite et réessayable.
Ce que les CDN et les proxys font avec ces en-têtes
Les caches partagés se placent entre votre origine et vos clients, et ils lisent les mêmes en-têtes selon leurs propres règles.
privateexclut entièrement une réponse de la mise en cache CDN ;s-maxage=600définit un TTL spécifique au CDN plus long ou plus court que le `max-age` du navigateur.- La plupart des CDN revalident avec votre origine en utilisant des requêtes conditionnelles. Si votre origine répond à If-None-Match par un 304, le CDN rafraîchit sa copie stockée sans récupérer le corps. Les ETags rendent également votre CDN moins cher.
- Confirmez toujours que votre framework envoie
Varycorrectement. Une API qui sert à la fois du JSON et du CSV à partir d'une seule URL a besoin deVary: Accept, sinon un cache partagé remettra du CSV à un client JSON. - Surveillez les proxys qui affaiblissent les ETags via la compression, comme mentionné ci-dessus.
Exemple Express : retourner un ETag et gérer If-None-Match
Express définit des ETags faibles de son propre chef, mais la gestion manuelle vous donne des ETags forts ainsi que le chemin d'écriture 412 :
import crypto from "node:crypto";
import express from "express";
const app = express();
app.use(express.json());
function etagFor(payload) {
const hash = crypto.createHash("sha1")
.update(JSON.stringify(payload))
.digest("hex");
return `"${hash}"`;
}
app.get("/v1/products/:id", async (req, res) => {
const product = await db.products.find(req.params.id);
const etag = etagFor(product);
res.set("Cache-Control", "private, max-age=60, stale-while-revalidate=120");
res.set("ETag", etag);
if (req.get("If-None-Match") === etag) {
return res.status(304).end(); // fingerprint matches: no body
}
res.json(product);
});
app.put("/v1/products/:id", async (req, res) => {
const product = await db.products.find(req.params.id);
const currentEtag = etagFor(product);
const ifMatch = req.get("If-Match");
if (!ifMatch) {
return res.status(428).json({ error: "If-Match header required" });
}
if (ifMatch !== currentEtag) {
return res.status(412).json({ error: "Resource changed since you fetched it" });
}
const updated = await db.products.update(req.params.id, req.body);
res.set("ETag", etagFor(updated));
res.json(updated);
});
Notez que la branche 304 envoie toujours les en-têtes Cache-Control et ETag. Conformément à la RFC 9111, un 304 met à jour les métadonnées de la réponse stockée, donc renvoyez tout ce dont le client a besoin pour maintenir sa copie à jour.
Vérification du comportement de mise en cache dans Apidog
Un code qui semble correct peut toujours mal mettre en cache une fois que les middlewares et les proxys sont impliqués. Testez au niveau HTTP, pas au niveau du code.
Dans Apidog, la vérification manuelle prend environ une minute :
- Envoyez
GET /v1/products/42et ouvrez le panneau des en-têtes de réponse. Confirmez queETagetCache-Controlsont présents et que l'ETag est entre guillemets. Copiez la valeur de l'ETag. - Sur la même requête, ajoutez un en-tête
If-None-Matchavec la valeur copiée et envoyez à nouveau. Vous devriez obtenir un 304 avec un corps vide. Si vous obtenez toujours un 200, votre couche de validation ne compare pas les empreintes numériques. - Modifiez l'enregistrement, renvoyez, et confirmez que vous êtes de retour à 200 avec un nouvel ETag.
Pour maintenir ce fonctionnement après chaque déploiement, intégrez le même flux dans un scénario de test. Enchaînez deux requêtes : la première extrait l'ETag des en-têtes de réponse dans une variable, la seconde le renvoie en tant que If-None-Match et affirme que le statut est 304 et que le corps est vide. Ajoutez une troisième étape pour le chemin d'écriture : envoyez un PUT avec une valeur If-Match délibérément périmée comme "deadbeefcafe1234" et affirmez 412. Notre guide sur les assertions API couvre la syntaxe d'assertion pour les codes de statut et les en-têtes.
Exécutez ce scénario en CI et une mise à jour de middleware qui supprime silencieusement vos ETags devient un pipeline échoué au lieu d'une facture de bande passante. Téléchargez Apidog gratuitement et construisez le scénario sur vos propres points de terminaison ; il faut plus de temps pour lire à ce sujet que pour le créer en quelques clics.
FAQ
Quelle est la différence entre no-cache et no-store ?
no-store interdit complètement la mise en cache : rien n'est écrit sur disque ou en mémoire, donc chaque requête télécharge la réponse complète. no-cache autorise le stockage mais force la revalidation avant chaque réutilisation, donc associé à un ETag, il produit toujours des réponses 304 et des économies de charge utile. Utilisez no-store uniquement pour les données sensibles. L'utiliser partout est la plus coûteuse erreur Cache-Control qu'une équipe API puisse faire.
Les ETags fonctionnent-ils avec POST ?
Principalement non, et c'est voulu. Les ETags décrivent l'état d'une ressource à une URL, et POST crée généralement quelque chose de nouveau plutôt que de lire un état stable. En pratique, les caches ne mettent pas en cache les réponses POST. Les en-têtes conditionnels qui importent pour les écritures sont If-Match sur PUT, PATCH et DELETE, où l'ETag protège contre les mises à jour perdues. Si vous êtes tenté de mettre en cache les réponses POST, c'est généralement un signe que l'opération devrait être un GET.
Une réponse 304 rend-elle mon API plus rapide ?
Elle rend les transferts plus petits, ce qui n'est pas la même chose. Le serveur reçoit toujours la requête, exécute l'authentification et calcule l'ETag actuel, de sorte que les économies de CPU côté origine dépendent de la facilité avec laquelle vous dérivez cette empreinte numérique. Les gains se manifestent en termes de bande passante, de batterie mobile et de temps de rendu sur les réseaux lents. Mesurez avant et après ; notre guide de test de performance API montre comment évaluer la latence et le débit afin que vous puissiez prouver la différence au lieu de la deviner.
Dois-je utiliser ETag ou Last-Modified ?
Envoyez les deux lorsque vous le pouvez. L'ETag est plus précis : il détecte les changements en deçà de la seconde et les différences au niveau du contenu qu'un horodatage manquerait, et If-None-Match a la priorité sur If-Modified-Since lorsque les deux arrivent. Last-Modified reste utile comme solution de secours pour les clients plus anciens et comme heuristique que certains caches utilisent pour estimer la fraîcheur. Si vous ne devez en envoyer qu'un, envoyez ETag.
