Construire · Avancé

Mettre en cache des prix propres à chaque client : quand un site rapide affiche le mauvais prix

Un cache de pages stocke une réponse et la remet à de nombreux visiteurs. Si cette réponse contient un prix propre à un client ou des informations de compte, le visiteur suivant les reçoit aussi.

8 min de lecture

Des rangées de serveurs avec leurs câbles réseau dans un centre de données.

Photo : Derrick Coetzee, domaine public (CC0)

Dans ce guide

Comment un cache de pages sert le prix d’un client à un autre

Un cache stocke une réponse sous une clé de cache et renvoie la copie stockée à toute requête ultérieure qui produit la même clé. Par défaut, la clé est construite à partir de la méthode de la requête et de l’URL : schéma, hôte, chemin et chaîne de requête.

Prenez une boutique B2B où le client A bénéficie d’une remise contractuelle. A est connecté et demande une page produit. L’application lit le cookie de session et génère la page avec le prix de A. Un cache placé devant l’application stocke cette réponse sous la clé de l’URL du produit. Le client B demande la même URL. La clé est identique, le cache renvoie donc la page stockée, avec le prix de A et peut-être le nom de l’entreprise de A, sans interroger l’application.

L’application a calculé le bon prix et le cache a fait ce pour quoi il était configuré. Le défaut, c’est que la réponse dépendait d’une chose que la clé ne contenait pas : qui la demandait. Toutes les fuites de ce genre ont cette forme. Une réponse varie selon une dimension, comme le client, la grille tarifaire ou la devise, et la clé ne la contient pas.

Caches partagés et cache privé du navigateur

  • Un cache partagé stocke des réponses pour de nombreux utilisateurs : un CDN, un proxy inverse devant le serveur web, ou un cache de pages intégré au serveur ou à l’application.
  • Un cache privé appartient à un seul utilisateur. En pratique, c’est le cache du navigateur.

Une réponse personnelle dans un cache partagé est une fuite entre clients. Dans le cache du navigateur, elle reste chez cet utilisateur, mais elle peut encore apparaître après la déconnexion, par exemple avec le bouton Précédent sur un ordinateur que plusieurs personnes utilisent.

HTTP possède une protection intégrée : la réponse à une requête qui portait un en-tête Authorization ne doit pas être réutilisée par un cache partagé, sauf si la réponse l’autorise explicitement. Les cookies n’ont aucune protection de ce genre. Pour un cache, un cookie de session ne signifie rien. La plupart des boutiques et des portails conservent la connexion dans un cookie : le garde-fou doit donc venir de vos propres en-têtes et de la configuration du cache.

Les caches de pages intégrés aux applications et aux modules de serveur suivent souvent leurs propres règles, par exemple « ne pas mettre en cache quand un cookie de connexion est présent ». Si votre connexion utilise un cookie que le cache ne connaît pas, cette règle ne s’applique jamais.

Les en-têtes HTTP qui décident qui peut stocker une réponse

L’origine indique aux caches ce qu’ils ont le droit de faire au moyen de l’en-tête Cache-Control de la réponse.

  • private : la réponse est destinée à un seul utilisateur. Un cache partagé ne doit pas la stocker, le navigateur le peut.
  • public : n’importe quel cache peut stocker la réponse, même une réponse qui ne pourrait sinon être mise en cache qu’en privé.
  • no-store : aucun cache, partagé ou privé, ne devrait stocker la réponse.
  • no-cache : la réponse peut être stockée, mais elle doit être revalidée auprès de l’origine avant chaque réutilisation.
  • s-maxage : le nombre de secondes pendant lesquelles la réponse est considérée comme fraîche dans les caches partagés, où cette valeur remplace max-age. Les navigateurs l’ignorent.

Une réponse qui ne porte aucune de ces directives n’est pas automatiquement sûre. Les caches peuvent estimer une durée de fraîcheur quand l’origine n’en donne pas, et beaucoup de proxys et de CDN sont configurés avec des règles qui prennent le pas sur les en-têtes de l’origine. Une règle « tout mettre en cache », ajoutée pour accélérer une boutique, peut réduire à néant un Cache-Control: private pourtant correct.

Surveillez aussi Set-Cookie. Si un cache partagé stocke une réponse qui pose un cookie de session, les visiteurs suivants peuvent recevoir ce cookie. Les réponses qui ouvrent ou modifient une session devraient porter no-store.

Pourquoi Vary: Cookie est un outil grossier

L’en-tête de réponse Vary nomme les en-têtes de requête qui entrent dans la clé. Vary: Cookie ressemble donc à la solution : une copie par valeur de cookie.

C’est rarement une bonne solution. Le cache compare l’en-tête Cookie tout entier, qui transporte aussi les cookies de statistiques et de consentement, différents pour presque chaque visiteur. Les visiteurs anonymes, qui pourraient tous partager une seule copie, ont chacun la leur, et le taux de succès du cache s’effondre. Les pages personnelles restent stockées dans un cache partagé, à raison d’une copie par client. Et certains CDN ne prennent Vary en charge que pour certains en-têtes : lisez la documentation du vôtre et testez.

Frontières de session : découper la page, pas le cache

La conception fiable sépare ce qui est identique pour tout le monde de ce qui est personnel, et envoie chaque partie avec sa propre règle.

  1. Générez la page sans rien de personnel : données du produit, images, descriptions, et soit le prix public, soit aucun prix. Envoyez-la avec public et s-maxage pour que le cache partagé puisse la servir à tout le monde.
  2. Chargez les parties personnelles dans une requête séparée, une fois la page arrivée : prix du client, nom du compte, nombre d’articles dans le panier. Envoyez ces réponses avec private, no-store.
  3. Vérifiez la session côté serveur pour chaque requête personnelle.

Si les prix dépendent d’une grille tarifaire commune à de nombreux clients, l’identifiant de la grille peut être ajouté à la clé de cache, pour que chaque groupe ait sa propre copie. Ce n’est sûr que si l’identifiant vient de la session vérifiée. Un simple cookie comme price_group=3, que le visiteur peut modifier, permet à n’importe qui de demander la page en cache d’un autre groupe.

Une frontière plus simple est tout aussi valable : contourner le cache partagé pour toute requête qui porte un cookie de session. Cela fonctionne quand les clients connectés représentent une petite part du trafic. Pour un système de commandes B2B très fréquenté, où presque tous les visiteurs sont connectés, la conception découpée compte davantage.

Mettre en cache des prix propres à chaque client : une règle par type de page

Les types de pages et la règle de cache qui leur convient
Type de page Contenu personnel Cache partagé Cache-Control de la réponse
CSS, JavaScript et images versionnés Aucun Oui public, max-age=31536000, immutable
Pages de contenu et de catalogue sans prix personnels Aucun Oui, purge à chaque modification public, s-maxage défini, max-age court
Squelette de page produit, prix chargé séparément Aucun dans le squelette Oui public, s-maxage défini, max-age court
Requête du prix client ou du nombre d’articles du panier Prix, panier Non private, no-store
Page produit générée avec le prix du client Prix Non private, no-store
Panier, validation de commande, compte, commandes, factures Tout Non no-store
Connexion, déconnexion, toute réponse qui pose un cookie de session Session Non no-store

Tester avec deux clients et un visiteur anonyme

Cette fuite n’apparaît pas quand un seul développeur teste avec un seul compte. Le test demande deux clients aux prix différents, et il doit passer par la même chaîne de caches que la production, pas aller directement à l’application.

  1. Choisissez un produit pour lequel le client A et le client B ont des prix différents. Purgez le cache pour son URL, ou attendez qu’il expire.
  2. Demandez la page en tant que client A. Relevez le prix affiché, les éventuelles informations de compte et les en-têtes de la réponse.
  3. Demandez la même URL en tant que client B, depuis un autre profil de navigateur ou un autre client HTTP. Puis demandez-la sans aucun cookie.
  4. Comparez les trois réponses. B doit voir le prix de B, et le visiteur anonyme ne doit voir ni prix client, ni nom de compte, ni cookie de session de qui que ce soit d’autre.
  5. Lisez les en-têtes : Cache-Control, Vary, Set-Cookie et Age. Un en-tête Age sur une réponse personnelle signifie qu’elle vient d’un cache. Beaucoup de CDN et de proxys ajoutent aussi un en-tête qui indique si la réponse a été trouvée en cache ou non.
  6. Recommencez en changeant l’ordre : anonyme d’abord, puis A, puis B. C’est le premier demandeur qui remplit le cache, chaque ordre teste donc une défaillance différente.
  7. Appliquez la même séquence aux requêtes séparées du prix et du panier, puis déconnectez-vous et utilisez le bouton Précédent sur les pages du compte pour vérifier le cache du navigateur.

Intégrez la séquence aux tests automatisés qui tournent avant chaque mise en production, et relancez-la après toute modification des réglages du CDN, du proxy ou du cache. Une amélioration des performances est le moment où ce défaut a le plus de chances d’être introduit, et les portails clients ont besoin du même test que les boutiques.

Les questions qu’on nous pose souvent

Il empêche les caches partagés qui se comportent correctement de stocker la page, mais le navigateur peut encore la conserver. Pour les prix et les données de compte, private, no-store est le choix le plus sûr, et il vous reste à vérifier qu’aucune règle de CDN ou de proxy ne prend le pas sur l’en-tête.

Oui. Mettez dans le cache partagé les parties identiques pour tout le monde et chargez les parties personnelles séparément. Les fichiers statiques et le squelette de la page viennent alors du cache, et seules de petites requêtes personnelles atteignent l’application.

Oui. Un proxy inverse devant le serveur web, un module de cache de pages dans le serveur ou une extension de cache dans l’application sont tous des caches partagés, et ils peuvent servir la page d’un client à un autre.

Vous préférez que ce soit réglé sans vous en charger ?

Dites-nous sur quoi vous travaillez. Une personne vous répond sous un jour ouvré.