
Dans une architecture qui mêle Drupal et des composants Symfony — API Platform, microservices, bundles maison — la question du cache HTTP se pose très tôt. Et souvent trop tard : quand des données périmées s'affichent en production, quand un rollback déclenche une avalanche de requêtes, ou quand personne ne sait pourquoi le CDN retient des pages que Varnish devrait avoir purgées.
La cause est presque toujours la même : des headers émis par des couches différentes, sans stratégie commune. Drupal envoie ses Cache-Control. Symfony envoie les siens. Varnish interprète, parfois de travers. Et le résultat, c'est un système de cache qui fonctionne en théorie mais se contredit en pratique.
Cet article pose une stratégie cohérente : un seul vocabulaire de headers, une politique d'invalidation explicite, et des exemples de configuration Varnish adaptables à votre contexte.
Ce que chaque couche comprend (et ne comprend pas) de l'autre
Drupal et le cache interne
Drupal dispose d'un système de cache sophistiqué : cache des rendus, cache des entités, cache des routes, cache des blocs. Chaque élément peut porter des cache tags (étiquettes qui permettent une invalidation ciblée) et des cache contexts (variations selon l'utilisateur, la langue, l'URL).
Par défaut, Drupal émet des headers HTTP corrects pour les pages anonymes : Cache-Control: max-age=3600, public avec un X-Drupal-Cache: HIT ou MISS. Le module Internal Page Cache et le module Dynamic Page Cache agissent en amont.
Mais dès qu'un composant Symfony entre dans la chaîne — un endpoint d'API, un bundle d'authentification, un service de données — ces headers peuvent diverger. Symfony a sa propre API de réponse HTTP (Response::setCache()), qui n'est pas connectée au système de tags Drupal.
Symfony et la liberté des headers
Symfony n'impose rien. C'est sa force et sa faiblesse dans ce contexte. Un contrôleur qui ne positionne pas explicitement ses headers HTTP retourne une réponse sans directive de cache — ce qui se traduit, côté Varnish, par un comportement indéfini : mise en cache indésirable ou absence de cache là où on en voudrait.
Le HttpCache de Symfony est un proxy en PHP, utile en développement, mais rarement utilisé en production devant Varnish. Ce qui compte ici, c'est la réponse que votre contrôleur ou votre service API expose au proxy.
Varnish et sa logique d'interprétation
Varnish lit les headers HTTP et prend ses décisions selon la configuration VCL (Varnish Configuration Language). Par défaut (VCL 4.x), Varnish ne met pas en cache une réponse si :
- le header
Set-Cookieest présent ; Cache-Controlcontientprivate,no-cache, ouno-store;- la méthode HTTP n'est pas
GETouHEAD.
Ce comportement est sain, mais il signifie que toute réponse Drupal ou Symfony qui laisse passer un cookie de session — même inutile — court-circuite le cache pour cette URL.
La stratégie unifiée : un seul contrat de headers
Pour qu'un système multi-couches fonctionne de façon cohérente, chaque composant doit respecter le même contrat. Voici les quatre directives à piloter ensemble.
Cache-Control : la directive maîtresse
Définissez une politique par type de contenu, appliquée uniformément :
- Pages publiques statiques :
public, max-age=3600, s-maxage=86400 - Pages avec données utilisateur :
private, no-store - Réponses API publiques :
public, max-age=60, s-maxage=300 - Réponses API authentifiées :
private, no-cache
s-maxage s'adresse spécifiquement aux caches partagés (Varnish, CDN) et prend le dessus sur max-age pour eux. C'est le levier principal pour découpler la durée côté navigateur de la durée côté proxy.
ETag : la revalidation propre
L'ETag permet à un client ou à Varnish de vérifier si une ressource a changé sans retransférer son contenu complet. Drupal génère des ETags sur ses pages. Symfony les génère si vous appelez Response::setETag().
Le problème dans un contexte mixte : deux composants peuvent générer des ETags avec des algorithmes différents pour la même ressource logique. La règle est simple — un seul composant est responsable de l'ETag par type de ressource. Si l'API Symfony expose une entité Drupal, c'est Drupal qui définit l'ETag, pas Symfony.
Surrogate-Control et Surrogate-Key : l'invalidation ciblée
Surrogate-Control est la directive que Varnish lit en priorité devant Cache-Control. Elle permet de piloter le proxy séparément du navigateur :
Surrogate-Control: max-age=86400 / Cache-Control: public, max-age=60
Ici, le navigateur rafraîchit toutes les heures, mais Varnish peut garder la réponse 24 heures — et l'invalider à la demande via une purge ciblée.
Surrogate-Key est le mécanisme complémentaire : chaque réponse porte des étiquettes, et une purge sur une étiquette invalide toutes les réponses qui la portent. Drupal utilise ses cache tags nativement pour ça. La brique à ajouter côté Symfony : émettre les mêmes étiquettes dans le header Surrogate-Key pour les ressources qui dépendent d'entités Drupal.
Vary : les variations légitimes
Vary indique à Varnish quels headers doivent déclencher des entrées de cache séparées. Les cas courants : Vary: Accept-Language pour un site multilingue, Vary: Accept-Encoding pour la compression. À éviter : Vary: Authorization — Varnish crée une entrée par valeur de header, ce qui explose la taille du cache. Préférez une politique private pour les réponses authentifiées.
Configuration Varnish : base solide pour un contexte Drupal/Symfony
Un squelette VCL 4.x adapté à cette stratégie fait trois choses importantes : il supprime les cookies inutiles avant de décider de mettre en cache, il lit Surrogate-Control en priorité, et il bloque la mise en cache des réponses privées quelle que soit leur origine — Drupal ou Symfony.
Les points clés de la VCL : strip des cookies analytics sur les URLs publiques, passage en pass pour toute requête avec Authorization ou cookie de session Drupal, lecture du TTL depuis Surrogate-Control (puis fallback sur Cache-Control), et suppression de Surrogate-Control avant la livraison au client.
Invalidation : la partie la plus difficile
Un cache bien configuré est inutile si son invalidation est chaotique. Les deux pathologies fréquentes :
Invalidation trop large. Un événement de déploiement déclenche un purge global. Toutes les entrées du cache disparaissent. Le premier pic de trafic suivant génère une avalanche de requêtes vers le backend — le thundering herd. La parade : les purges ciblées par Surrogate-Key.
Invalidation silencieuse. Un composant Symfony met à jour une donnée sans notifier Drupal, qui ne régénère pas ses cache tags. La version périmée reste en cache jusqu'à expiration naturelle. La parade : un événement Symfony (Doctrine PostFlushEventArgs, par exemple) qui déclenche une invalidation de tag côté Drupal via son API ou un appel HTTP à Varnish.
Pour les rollbacks, la règle est simple : un rollback n'invalide jamais moins que le dernier déploiement. Si le déploiement avait invalidé les tags node:article et taxonomy:categorie, le rollback doit faire de même.
Ce que ça change en pratique
Une stratégie de cache unifiée entre Drupal, Symfony et Varnish, c'est moins de surprises en production : les durées de fraîcheur sont prévisibles, les purges sont chirurgicales, et un rollback ne transforme pas le proxy en champ de mines.
La mise en place demande un audit préalable des headers émis par chaque composant, une révision de la VCL, et souvent quelques ajouts côté Symfony pour émettre les bons tags. Ce n'est pas trivial dans un environnement existant — mais c'est beaucoup moins coûteux que de diagnostiquer un incident de cache en pleine nuit.
Un projet web en tête ?
Commençons par un diagnostic : ce qui vous ralentit, et par où commencer.
Demander un diagnostic