Comprendre le cache HTTP et l'en-tête Cache-Control
Une réponse HTTP ne se contente pas d'être envoyée : elle indique aussi qui a le droit de la garder, combien de temps et à quelles conditions la réutiliser. C'est le rôle de l'en-tête Cache-Control, dont les règles sont fixées par la RFC 9111. Comprendre ces règles évite les deux erreurs symétriques du web moderne, servir du contenu périmé et refaire travailler le serveur pour rien.
Deux familles de caches, deux comportements
La RFC 9111, publiée en juin 2022 sous le numéro STD 98, remplace la RFC 7234 et définit ce qu'est un cache HTTP ainsi que les en-têtes qui le pilotent. Elle distingue deux familles. Le cache privé appartient à un seul utilisateur : c'est le cache du navigateur, qui peut conserver une réponse personnalisée parce que personne d'autre ne la lira. Le cache partagé se place entre le serveur d'origine et plusieurs utilisateurs : mandataire d'entreprise, CDN, mandataire inverse. Une seule copie y sert tout le monde, ce qui interdit d'y laisser traîner du contenu propre à un visiteur.
Les deux familles ne lisent pas la même information. La clé de cache est construite au minimum avec la méthode et l'adresse demandée, mais la plupart des caches courants ne stockent que des réponses à des requêtes GET et se contentent donc de l'adresse. Quand un serveur négocie le contenu, par exemple selon la langue ou le format d'image accepté, l'en-tête Vary indique quels champs de la requête ont influencé la réponse : le cache conserve alors une variante par valeur de ces champs. Vary doit porter la même valeur sur toutes les réponses d'une adresse, y compris les 304, sans quoi les variantes se mélangent.
Un point de départ souvent oublié : la mise en cache est une fonction facultative de HTTP, mais la réutilisation d'une réponse stockée est le comportement attendu dès qu'aucune contrainte ne l'interdit. Les exigences de la norme visent donc à empêcher de stocker une réponse inutilisable ou de la réutiliser à mauvais escient, pas à imposer un stockage. Un site qui ne dit rien laisse la main aux implémentations, et elles en usent.
Fraîcheur : la durée de vie réelle d'une réponse
Une réponse stockée est fraîche tant que son âge n'a pas dépassé sa durée de vie. La RFC 9111 fixe l'ordre d'évaluation, et le premier critère trouvé l'emporte : pour un cache partagé, la directive s-maxage ; sinon max-age ; sinon l'en-tête Expires moins la date de la réponse ; et si rien de tout cela n'est présent, le cache peut appliquer une fraîcheur heuristique. Le calcul se résume à comparer deux valeurs : la réponse est fraîche si sa durée de vie est supérieure à son âge courant.
L'âge ne part pas de l'arrivée dans le navigateur, mais de la génération par le serveur d'origine. Chaque cache traversé ajoute son temps de séjour, transmis par l'en-tête Age, si bien qu'une réponse passée par un CDN pendant cent secondes arrive dans le navigateur déjà vieille de cent secondes, et la durée de fraîcheur restante s'en trouve réduite d'autant. C'est ce mécanisme qui empêche une chaîne de caches de prolonger indéfiniment la vie d'un contenu.
Sans max-age ni Expires, un cache peut estimer une durée de vie à partir de l'en-tête Last-Modified : la RFC encourage une fraction de l'intervalle écoulé depuis cette date, typiquement dix pour cent. Une page modifiée il y a un an peut ainsi être jugée bonne pour un mois environ, ce qui est une estimation, pas une décision du serveur. La norme interdit d'ailleurs d'utiliser cette heuristique dès qu'une expiration explicite existe, et conseille de considérer comme périmée toute réponse dont la valeur de max-age est invalide, par exemple négative ou décimale.
max-age, s-maxage, no-cache, no-store : les confusions coûteuses
max-age indique un nombre de secondes pendant lequel la réponse reste fraîche ; s-maxage fait de même, mais seulement pour les caches partagés, et il prend alors le pas sur max-age et sur Expires. Mieux : s-maxage intègre la sémantique de proxy-revalidate, ce qui interdit à un cache partagé de resservir une réponse périmée sans l'avoir validée. C'est la directive à regarder en premier quand un CDN semble garder une page trop longtemps.
La confusion la plus répandue concerne no-cache. Elle ne signifie pas « ne pas mettre en cache » : elle autorise le stockage, mais impose une validation auprès du serveur avant chaque réutilisation, même si le cache est déconnecté. Pour empêcher réellement tout stockage, il faut no-store. Entre les deux, private limite la réponse au cache du navigateur, ce qui est la bonne réponse pour un contenu personnalisé, tandis que public autorise explicitement les caches partagés à conserver la réponse même si elle aurait été difficile à juger cachable autrement.
Deux détails pratiques découlent de ces définitions. D'abord, en cas de directives contradictoires, comme max-age et no-cache ensemble, la plus restrictive doit être honorée. Ensuite, la durée indiquée par max-age est plafonnée par la représentation des nombres : une valeur supérieure à ce qu'un cache peut stocker doit être ramenée à 2 147 483 648 secondes, soit environ soixante-huit ans, ce qui revient à dire « très longtemps » sans garantie d'éternité.
Revalidation : ETag, Last-Modified et la réponse 304
Quand une réponse n'est plus fraîche, le cache n'est pas obligé de retélécharger le contenu. Il peut poser une question au serveur sous forme de requête conditionnelle. Deux validateurs servent à cela : l'ETag, une étiquette propre à une version du contenu, envoyée dans If-None-Match, et la date Last-Modified, envoyée dans If-Modified-Since. Si le contenu n'a pas changé, le serveur répond 304 Not Modified, sans corps, et le cache réutilise sa copie en la rafraîchissant.
L'ordre de priorité compte : quand les deux validateurs sont disponibles, If-None-Match l'emporte sur If-Modified-Since. Un ETag fort change dès qu'un octet du contenu change, ce qui en fait un signal plus fiable qu'une date à la seconde près. Une réponse 304 peut aussi porter de nouveaux en-têtes, notamment un Cache-Control actualisé : c'est ainsi qu'un serveur prolonge la durée de vie d'une ressource sans renvoyer son contenu.
Cette mécanique explique pourquoi un cache bien réglé n'est pas un cache figé. Une page d'accueil peut avoir une durée de vie courte, quelques minutes, tout en coûtant très peu à revalider : la réponse 304 pèse quelques centaines d'octets au lieu de tout le document. À l'inverse, une ressource dont l'adresse change à chaque version, comme un fichier dont le nom contient une empreinte, peut recevoir une durée de vie très longue sans risque, puisque la nouvelle version aura une autre adresse.
must-revalidate, stale-while-revalidate et le contenu périmé
Par défaut, un cache ne doit pas servir une réponse périmée sans autorisation, sauf s'il est déconnecté du serveur d'origine. La directive must-revalidate ferme cette tolérance : une fois la réponse périmée, elle doit être validée avant toute réutilisation, même si le serveur est injoignable. C'est le réglage prudent pour une page dont l'exactitude compte, par exemple une page de tarifs ou une page d'état de service.
Deux extensions, définies hors de la RFC 9111, assouplissent cette règle de façon contrôlée. stale-while-revalidate, décrite dans la RFC 5861, autorise un cache à servir immédiatement une copie périmée pendant une durée limitée tout en la rafraîchissant en arrière-plan : l'utilisateur n'attend pas, et la version suivante est à jour. stale-if-error permet de servir une copie périmée quand le serveur d'origine renvoie une erreur, ce qui évite une page blanche pendant un incident.
Il existe aussi une directive d'immutabilité, décrite dans la RFC 8246 : une réponse déclarée immutable promet de ne pas changer pendant sa durée de vie, ce qui dispense le navigateur de revalider lors d'un rechargement. Elle n'a de sens que sur des fichiers dont l'adresse change avec le contenu. Appliquée à un document HTML dont l'adresse reste stable, elle transforme une erreur de réglage en contenu impossible à corriger pour les visiteurs déjà passés.
Ce que change un cache intermédiaire, notamment un CDN
Un CDN est un cache partagé, placé au plus près des visiteurs. Sa présence modifie deux choses. D'abord, la réponse peut être servie sans que le serveur d'origine soit sollicité, ce qui réduit la charge et la latence, mais signifie aussi que le serveur ne voit plus la requête : les journaux du CDN deviennent la seule trace de ce trafic. Ensuite, la durée de vie perçue par le visiteur dépend de la fraîcheur restante au moment où le CDN la lui transmet, donc de s-maxage autant que de max-age.
La norme HTTP ne définit aucune méthode pour supprimer une entrée de cache à la demande. La purge existe parce que les caches gérés, eux, l'ajoutent : tableau de bord, appel d'API, redémarrage. C'est ce qui autorise une stratégie plus agressive sur les documents principaux, avec une durée de vie longue et une purge au moment de la mise à jour. À noter aussi que certains caches gérés peuvent ignorer volontairement no-store : la documentation du produit employé fait alors foi, et des en-têtes propres à un CDN, comme ceux normalisés par la RFC 9213, servent à distinguer ce que voit le CDN de ce que voient les navigateurs.
Vérifier ses en-têtes plutôt que les supposer
Trois contrôles suffisent à savoir ce qu'un site annonce vraiment. Une requête d'en-têtes sur l'adresse concernée montre le Cache-Control, l'ETag, le Last-Modified et, le cas échéant, l'Age. Une seconde requête juste après indique si la réponse vient du cache : la présence d'un Age qui augmente, ou d'un indicateur de succès de cache propre au CDN, le confirme. Enfin, une requête conditionnelle avec l'ETag reçu doit produire un 304, ce qui prouve que la revalidation fonctionne et que le serveur ne renvoie pas tout le document à chaque fois.
Ce contrôle doit se faire sur l'adresse réellement servie aux visiteurs, pas sur un environnement de test dont les en-têtes diffèrent. Il faut aussi tenir compte des couches : un CDN peut réécrire le Cache-Control reçu de l'origine, et un hébergement mutualisé peut ajouter ses propres règles. Enfin, une réponse peut être mise en cache par le navigateur alors que la requête réseau, elle, a bien eu lieu : la navigation dans l'historique et le cache arrière-avant restaurent un instantané de la page sans revalidation, ce que no-cache ne couvre pas.
Les erreurs de réglage les plus fréquentes
Quatre réglages reviennent le plus souvent en audit. Un contenu personnalisé, panier ou espace client, servi avec public ou sans private, peut être stocké par un cache partagé et montré à un autre visiteur : c'est une fuite de données, pas seulement un défaut de performance. Une durée de vie longue appliquée à des documents HTML dont l'adresse ne change pas fige le site chez les visiteurs. Un no-cache posé partout pour se rassurer supprime le bénéfice du cache sans rien sécuriser de plus qu'une revalidation correctement configurée. Enfin, un Vary absent sur une réponse négociée fait servir la mauvaise variante, par exemple une page dans une langue pour l'autre.
La méthode inverse est plus rentable : décider par type de ressource. Les fichiers dont l'adresse contient une empreinte de version peuvent être conservés très longtemps, sans revalidation. Les documents HTML, dont l'adresse est stable, gagnent à une durée de vie courte accompagnée d'un ETag, ce qui rend la revalidation presque gratuite. Les réponses personnalisées restent privées et non stockées par les intermédiaires. Ce classement se vérifie ensuite par des mesures réelles, car une durée de vie n'améliore rien si la ressource change plus vite qu'elle n'expire.