
# Pourquoi votre CDN ne met rien en cache

Tous les fichiers statiques d'agentready.md revenaient avec la même ligne :

```
cf-cache-status: BYPASS
```

Polices, feuilles de style, scripts, images. Pas `MISS`, qui voudrait dire que le edge n'avait pas encore vu le fichier et va garder la copie qu'il vient de chercher. `BYPASS` veut dire que le edge a lu la réponse, a décidé qu'il n'avait pas le droit de la stocker, et décidera la même chose demain. Chaque visiteur tirait tout depuis l'origine, et aurait continué indéfiniment.

## Les quatre réglages qui n'étaient pas en cause

- **Development Mode** — désactivé. Il coupe entièrement le cache du edge pendant trois heures, et on l'oublie souvent allumé : à vérifier en premier.
- **Caching Level** — Standard.
- **Page Rules** — aucune ne mettait Cache Level sur Bypass.
- **Browser Cache TTL** — une valeur raisonnable.

Tout ce que le tableau de bord avait à dire sur le cache disait *garde ça*. Le edge ne gardait rien.

## La réponse était dans la réponse HTTP

Elle saute aux yeux dès qu'on lit le bloc d'en-têtes en entier au lieu de la seule ligne qu'on était venu chercher :

```bash
curl -sI https://agentready.md/fonts/ibm-plex-sans-latin.woff2
```

```
HTTP/2 200
content-type: font/woff2
cache-control: public, max-age=31536000, immutable
set-cookie: _csrf=8f3c…; Path=/; SameSite=Strict
cf-cache-status: BYPASS
```

La troisième ligne demande un an. La quatrième explique pourquoi personne ne l'accorde.

**Une réponse porteuse de `Set-Cookie` n'est pas stockable dans un cache partagé.** Un cookie appartient à un visiteur ; stocker cette réponse et la servir au suivant, c'est lui remettre le cookie de quelqu'un d'autre. Tous les CDN refusent. Cloudflare donne un nom à ce refus, `BYPASS` ; d'autres se contentent de ne rien stocker, sans le dire.

Le cookie venait de chez nous. Un middleware générait un jeton CSRF à chaque requête et l'attachait, pour qu'un formulaire rendu n'importe où sur le site en trouve un tout prêt. « Chaque requête » incluait celle d'une police woff2 : un fichier qui n'affiche aucun formulaire, ne lit aucun jeton et n'exécute aucun code.

## Le correctif

Le CSRF reste tel quel. On cesse simplement de distribuer un jeton à ce qui ne peut pas s'en servir :

```js
const ASSET_PREFIXES = ['/css/', '/js/', '/fonts/', '/images/'];
const COOKIELESS_FILES = new Set([
  '/favicon.ico', '/badge.js', '/robots.txt', '/sitemap.xml', '/llms.txt',
]);

function servesWithoutCookie(path) {
  return COOKIELESS_FILES.has(path)
    || ASSET_PREFIXES.some((prefix) => path.startsWith(prefix))
    || path.endsWith('.md');
}
```

Comparez des chemins, pas des extensions. Certaines des réponses qui réclament le plus de cache sont générées et non lues sur disque — le jumeau Markdown de chaque page, nos badges SVG — et elles non plus n'affichent jamais de formulaire.

Si le cookie n'est pas de vous, regardez du côté des sessions. Beaucoup de frameworks ouvrent une session, et envoient son cookie, à chaque requête qui traverse le middleware, même quand rien n'y a jamais été écrit.

## Le diagnostic, sur n'importe quelle stack

Testez un asset, pas la page d'accueil. Une page d'accueil peut être non cacheable pour de bonnes raisons, et elle ne dit rien des trente fichiers qu'elle entraîne avec elle.

```bash
for u in / /css/app.css /js/app.js /fonts/ibm-plex-sans-latin.woff2; do
  echo "== $u"
  curl -sI "https://exemple.com$u" \
    | grep -iE 'cache-control|set-cookie|cf-cache-status|x-cache|^age'
done
```

Demandez deux fois la même URL. La seconde doit être un hit : `cf-cache-status: HIT`, `x-cache: Hit from cloudfront`, ou un en-tête `age:` qui monte sur Varnish et nginx. Un second miss signifie que l'objet n'est pas conservé, et la raison est dans les en-têtes que vous venez d'afficher.

## Les autres suspects habituels

`Cache-Control: private` — les navigateurs seulement, jamais un cache partagé. `no-store` — personne, pas même le navigateur. `no-cache` ne veut pas dire « ne mets pas en cache » mais « revalide avant chaque réutilisation », ce qui reste un aller-retour par fichier.

`Vary: *` rend la réponse non cacheable, point. `Vary: Cookie` est presque aussi mauvais : la clé de cache contient alors une valeur qui change à chaque visiteur, donc plus rien n'est partagé. `Vary: User-Agent` éclate un fichier en des milliers de variantes. `Accept-Encoding` est celui qui a sa place ici.

Les chaînes de requête. La plupart des caches utilisent l'URL entière comme clé : chaque variante `?utm_source=` ou `?fbclid=` d'une page est un objet distinct, tiré de l'origine. Nettoyez ou normalisez les paramètres de campagne au edge.

Et le réglage par défaut du framework, qui n'est pas un bypass mais coûte presque autant : `max-age=0` sur chaque fichier statique, c'est une requête de revalidation par asset et par navigation.

## Une politique de cache qui tient

La règle porte sur ce que promet l'URL, pas sur ce qu'est le fichier.

Une URL qui contient un hash du contenu (`app.css?v=9f2ac41b`, `app.9f2ac41b.css`) est une adresse de contenu : si le fichier change, l'URL change. Celles-là prennent `public, max-age=31536000, immutable`, et il n'y a jamais rien à purger. Le même fichier sans le jeton prend une heure : le jeton est la promesse, et rien ne se met en cache sur une promesse que personne n'a faite. Un logo ou une image sociale, remplacés sur place, prennent une semaine.

Reste l'exception, celle qu'on rate parce que le fichier a l'air d'être le plus cacheable qu'on possède : tout ce qui est embarqué dans les pages des autres, sur une URL que vous ne pouvez pas versionner. Nous servons `badge.js` avec une heure pour cette seule raison. Il tourne dans des sites dont nous ne maîtrisons ni le HTML ni l'URL : un an d'`immutable`, ce serait un bug hors d'atteinte pendant un an.

## Si vous servez des fichiers avec @fastify/static

Deux détails qui nous ont coûté un après-midi. Le module envoie `Cache-Control: public, max-age=0` par défaut. Et si vous posez l'en-tête vous-même via `setHeaders`, passez aussi `cacheControl: false`, sinon la bibliothèque fabrique le sien à partir de son `maxAge` par défaut et écrase le vôtre — sans un mot, en laissant quelque chose qui a l'air délibéré :

```js
app.register(fastifyStatic, {
  root: join(__dirname, 'public'),
  cacheControl: false,           // sinon setHeaders est écrasé
  setHeaders: setStaticCacheHeaders,
});
```

Avant de toucher au moindre réglage de cache, lisez une réponse en entier. L'en-tête qui casse le cache est rarement celui qui porte « cache » dans son nom.

[Analyser votre site](/fr) · [Checklist de préparation à l'IA](/fr/tools/ai-readiness-checklist) · [Les bots sont-ils autorisés ?](/fr/tools/ai-crawlers-checker)
