Date de publication du RFC : Juin 2022
Auteur(s) du RFC : R. Fielding (Adobe), M. Nottingham (Fastly), J. Reschke (greenbytes)
Chemin des normes
Réalisé dans le cadre du groupe de travail IETF httpbis
Première rédaction de cet article le 7 juin 2022
Le protocole HTTP transporte énormément de données tous les
jours et consomme donc à lui seul une bonne partie des ressources de
l'Internet. D'où l'importance de
l'optimiser. Une des méthodes les plus efficaces pour cela est le
cache (terme anglais qui
fait très bizarre en français : pour mieux accéder à une ressource,
on la cache...). Ce RFC spécifie le modèle de cachage (de
mémorisation ?) de HTTP et comment les clients et les serveurs
peuvent l'utiliser. Il remplace le RFC 7234
avec peu de modifications importantes (la plus spectaculaire étant
l'abandon du champ Warning:
dans
l'en-tête).
Un cache Web est un espace de stockage local où on peut conserver la représentation d'une ressource qu'on a récupérée. Si la même ressource est à nouveau désirée, on pourra la récupérer depuis cette mémoire, le cache, plus proche et donc plus rapide que le serveur d'origine. Outre le temps d'accès, le cachage a l'avantage de diminuer la consommation de capacité réseau. Un cache peut être partagé entre plusieurs utilisateurs, augmentant ainsi les chances qu'une ressource désirée soit présente, ce qui améliore l'efficacité. (S'il n'a qu'un seul utilisateur, on parle de cache privé.) Comme tous les caches, les caches Web doivent gérer le stockage, l'accès et la place disponible, avec un mécanisme pour gérer le cas du cache plein. Comme tous les caches, les caches Web doivent aussi veiller à ne servir que de l'information fraîche. Cette fraîcheur peut être vérifiée de différentes façons, y compris par la validation (vérification auprès du serveur d'origine). Donc, même si l'information stockée dans le cache n'est pas garantie fraîche, on pourra quand même l'utiliser, si le serveur d'origine confirme qu'elle est toujours utilisable (dans ce cas, on aura quand même un accès réseau distant à faire, mais on évitera de transférer une ressource qui peut être de grande taille).
Le cache est optionnel pour HTTP, mais recommandé, et utiliser un cache devrait être le comportement par défaut, afin d'épargner le réseau, pour lequel HTTP représente une bonne part du trafic.
On peut garder en cache plusieurs sortes de réponses HTTP. Bien
sûr, le résultat d'une récupération après un
GET
(code 200, cf. RFC 9110) est cachable et représente l'utilisation la plus
courante. Mais on peut aussi conserver dans le cache le résultat de
certaines redirections, ou bien des résultats négatifs (un code 410,
indiquant que la ressource est définitivement partie), ou même le
résultat de méthodes autres que GET
(bien que
cela soit plus rare en pratique).
Ici, un exemple où une page a été stockée par un cache
Squid, et récupérée ensuite. L'argument de
GET
est l'URI complet, pas juste le chemin :
% curl -v http://www.w3.org/WAI/ ... > GET http://www.w3.org/WAI/ HTTP/1.1 > User-Agent: curl/7.26.0 > Host: www.w3.org ... < HTTP/1.0 200 OK < Last-Modified: Thu, 12 Jun 2014 16:39:11 GMT < ETag: "496a-4fba6335209c0" < Cache-Control: max-age=21600 < Expires: Sun, 15 Jun 2014 15:39:30 GMT < Content-Type: text/html; charset=utf-8 < Age: 118 < X-Cache: HIT from cache.example.org < X-Cache-Lookup: HIT from cache.example.org:3128 < Via: 1.1 cache.example.org:3128 (squid/2.7.STABLE9) ... <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
Le client HTTP curl suit la
variable d'environnement
http_proxy
et contacte donc le relais/cache
Squid en cache.example.org
en HTTP. À son tour,
celui-ci se connectera au serveur d'origine si nécessaire (ce ne
l'était pas ici, l'information a été trouvée dans le cache, comme
l'indique la mention HIT.)
Les données stockées dans le cache sont identifiées par une
clé (section 2 de notre RFC). Pour un cache
simple, qui ne gère que GET
, la clé principale
est l'URI
de la ressource convoitée. On verra plus loin que la clé peut en
fait être plus complexe que cela, en raison de certaines fonctions
du protocole HTTP, comme la négociation de contenu, qui
impose d'utiliser comme clé certains en-têtes de la requête. A
priori, un cache ne mémorise que les réponses positives (codes de
retour 200) mais certains mémorisent des réponses négatives comme le
404 (ressource non trouvée).
La section 3 du RFC normalise les cas où le cache a le droit de stocker une réponse, pour réutilisation ultérieure. Le RFC définit ces cas négativement : le cache ne doit pas stocker une réponse sauf si toutes ces conditions sont vraies :
GET
),Cache-Control:
, voir plus
loin),Expires:
).Un cache peut stocker des réponses partielles, résultat de requêtes avec intervalles (cf. RFC 9110, section 14), si lui-même comprend ces requêtes. Il peut concaténer des réponses partielles pour ensuite envoyer une ressource complète.
Une fois la ressource stockée, le cache ne doit pas la renvoyer sauf si (là encore, la norme - section 4 - est formulée de manière négative, ce qui est déroutant) toutes ces conditions sont vraies :
Vary:
correspondent (cela concerne surtout le cas où il y a négociation du
contenu),
L'exigence sur la clé secondaire (les en-têtes sur lesquels se fait
la négociation de contenu) est là pour s'assurer qu'on ne donnera
pas à un client une ressource variable et correspondant aux goûts
d'un autre client. Si le client dont la requête a déclenché la mise
en cache avait utilisé l'en-tête Accept-Language:
fr
indiquant qu'il voulait du français, et que le second
client du cache demande la même ressource, mais avec
Accept-Language: en
, il ne faut évidemment pas
donner la copie du premier client au second. Si la réponse avait
l'en-tête Vary: accept-language
indiquant
qu'elle dépend effectivement de la langue, le cache ne doit la
donner qu'aux clients ayant le même
Accept-Language:
.
Et la fraîcheur, elle se définit comment (section 4.2, une des
plus importantes du RFC) ? Le cas le plus simple est celui où le
serveur d'origine envoie un en-tête Expires
(ou
une directive max-age
), par exemple
Expires: Mon, 15 Jun 2015 09:33:06 GMT
. Dans ce
cas, la ressource gardée en cache est fraîche jusqu'à la date
indiquée. Attention : les formats de date de HTTP sont compliqués
et il faut être prudent en les analysant. Si le
Expires:
indique une date syntaxiquement
incorrecte, le cache doit supposer le pire et considérer que la
ressource a déjà expiré. En pratique, bien des serveurs HTTP ne
fournissent pas cet en-tête Expires:
et le
cache doit donc compter sur des heuristiques. La plus courante est
d'utiliser le champ Last-Modified:
et de
considérer que, plus le document est ancien, plus il restera frais
longtemps (section 4.2.2). (La FAQ
de Squid explique bien l'heuristique de ce
logiciel de cache.) Le RFC ne normalise pas une heuristique
particulière mais met des bornes à l'imagination des programmeurs :
ces heuristiques ne doivent être employées que s'il n'y a pas de
date d'expiration explicite, et la durée de fraîcheur doit être
inférieure à l'âge du document (et le RFC suggère qu'elle ne soit
que 10 % de cet âge).
Dans sa réponse, le cache inclut un en-tête
Age:
, qui peut donner au client une idée de la
durée depuis la dernière validation (auprès du serveur
d'origine). Par exemple, Age: 118
, dans le
premier exemple, indiquait que la page était dans le cache depuis
presque deux minutes.
Une réponse qui n'est pas fraîche peut quand même être renvoyée
au client dans certains cas, notamment lorsque le cache est
déconnecté du réseau et ne peut pas donc valider que sa copie est
toujours bonne. Le client peut empêcher l'envoi de ces réponses
rassises avec Cache-Control: must-revalidate
ou
no-cache
.
Comment se fait cette validation dont on a déjà parlé plusieurs
fois ? Lorsque le serveur a une copie d'une ressource, mais que sa
date maximum de fraîcheur est dépassée, il peut demander au serveur
d'origine. Cela se fait typiquement par une requête conditionnelle
(cf. RFC 9110, section 13.1) : si le serveur a une copie
plus récente, il l'enverra, autrement, il répondra par un 304,
indiquant que la copie du cache est bonne. La requête conditionnelle
peut se faire avec un If-Modified-Since:
(RFC 9110, section 8.8.2) en
utilisant comme date celle qui avait été donnée dans le
Last-Modified:
. Ou bien elle peut se faire avec
l'entity tag (RFC 9110,
section 8.8.3) et un
If-None-Match:
:
% telnet cache 3128 ... GET http://www.w3.org/WAI/ HTTP/1.1 Host: www.w3.org If-None-Match: "496a-4fba6335209c0" HTTP/1.0 304 Not Modified Date: Sun, 15 Jun 2014 09:39:30 GMT Content-Type: text/html; charset=utf-8 Expires: Sun, 15 Jun 2014 15:39:30 GMT Last-Modified: Thu, 12 Jun 2014 16:39:11 GMT ETag: "496a-4fba6335209c0" Age: 418 X-Cache: HIT from cache.example.org X-Cache-Lookup: HIT from cache.example.org:3128 Via: 1.0 cache.example.org:3128 (squid/2.7.STABLE9) Connection: close
Le cache peut aussi utiliser la méthode HEAD
pour tester sa copie locale auprès du serveur d'origine, par exemple
pour invalider la copie locale, sans pour autant transférer la
ressource. Et s'il voit passer un URL connu avec des méthodes qui ont
de fortes chances de changer la ressource, comme
PUT
ou POST
, le cache doit
invalider la ressource stockée.
La section 5 liste tous les en-têtes des requêtes et des réponses
qui sont utilisés pour le bon fonctionnement des caches, comme
Age:
(en secondes), Expires:
, etc. Ils
sont enregistrés à
l'IANA, dans le registre des en-têtes (désormais séparé du registre
utilisé pour les en-têtes du courrier électronique).
Parmi ces en-têtes, Cache-Control:
permet de
spécifier des directives concernant le cache. Un client d'un cache
peut spécifier l'âge maximum qu'il est prêt à accepter (directive
max-age
), une fraîcheur minimum (directive
min-fresh
), que la ressource ne doit pas être
stockée dans le cache (directive no-store
, qui
est là pour des raisons de vie privée mais,
bien sûr, est loin de suffire pour une véritable confidentialité),
ou bien qu'elle peut être stockée mais ne doit pas être servie à un
client sans revalidation (directive no-cache
),
etc. Il y a aussi l'opposé de no-cache
,
only-if-cached
, qui indique que le client ne
veut la ressource que si elle est stockée dans le cache. (Attention,
dans un cache partagé, cela peut permettre à un client de voir ce que
les autres clients ont demandé, ce qu'on nomme le cache
snooping.) L'ensemble des directives possibles sont
stockées dans un
registre IANA. Ainsi, le RFC 8246 avait
ajouté une valeur possible à Cache-Control:
, pour
indiquer l'immuabilité d'une ressource.
L'en-tête
Cache-Control:
peut aussi être utilisé dans des
réponses. Un serveur peut lui aussi indiquer
no-cache
, typiquement parce que ce qu'il envoie
change fréquemment et doit donc être revalidé à chaque fois,
private
s'il veut insister sur le fait que la
réponse n'était destinée qu'à un seul utilisateur et ne doit donc
pas être transmise à d'autres (le RFC insiste que c'est une
protection vraiment minimale de la vie privée), etc.
À noter qu'un cache HTTP n'est pas forcément un serveur spécialisé. Tous les navigateurs Web ont des fonctions d'historique (comme le bouton Back). Est-ce que celles-ci nécessitent des précautions analogues à celles des caches, pour éviter que le navigateur ne serve des données dépassées ? Pas forcément, dit le RFC, qui autorise un navigateur à afficher une page peut-être plus à jour lorsqu'on utilise le retour en arrière dans l'historique (mais lisez la section 6 du RFC : cette autorisation vient avec des limites).
La section 7 détaille les problèmes de sécurité qui peuvent affecter les caches. Un cache, par exemple, peut permettre d'accéder à une information qui n'est plus présente dans le serveur d'origine, et donc de rendre plus difficile la suppression d'une ressource. Un cache doit donc être géré en pensant à ces risques. Plus grave, l'empoisonnement de cache : si un malveillant parvient à stocker une fausse représentation d'une ressource dans un cache (avec une longue durée de fraîcheur), tous les utilisateurs du cache recevront cette information au lieu de la bonne.
Un cache peut aussi avoir des conséquences pour la vie privée : en demandant une ressource à un cache partagé, un utilisateur peut savoir, à partir du temps de chargement et d'autres informations envoyées par le cache, si un autre utilisateur avait déjà consulté cette page. Et si un cache est privé (restreint à un·e seule·e utilisateurice), les données qu'il a stocké permettent d'avoir un panorama complet des activités Web de l'utilisateur.
L'annexe B liste les différences depuis le texte précédent, celui du RFC 7234 :
must-understand
, qui
permet au serveur d'indiquer que la ressource ne doit être
mémorisée que si le cache connait et comprend le code de retour indiqué,Warning
dans les réponses est abandonné, car il était peu utilisé et
souvent redondant avec l'information déjà présente dans la réponse.Question mise en œuvre, notez qu'il existe un projet de tests des caches pour vérifier leur conformité.
Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)
Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)