Perché il tuo CDN non mette niente in cache
Ogni file statico di agentready.md tornava con la stessa riga:
cf-cache-status: BYPASS
Font, fogli di stile, script, immagini. Non MISS, che vorrebbe dire che l'edge non aveva ancora visto il file e terrà la copia appena scaricata. BYPASS vuol dire che l'edge ha letto la risposta, ha deciso che non poteva conservarla, e domani deciderà lo stesso. Ogni visitatore si tirava giù tutto dall'origine, e avrebbe continuato a farlo per sempre.
Le quattro impostazioni che non c'entravano
- Development Mode — spento. Disattiva del tutto la cache dell'edge per tre ore e resta acceso per dimenticanza, quindi è la prima cosa da guardare.
- Caching Level — Standard.
- Page Rules — nessuna metteva Cache Level su Bypass.
- Browser Cache TTL — un valore sensato.
Tutto quello che il pannello aveva da dire sulla cache diceva conserva questo. L'edge non conservava niente.
La causa era nella risposta, non nel pannello
Salta fuori appena leggi il blocco di header per intero invece della sola riga che eri andato a cercare:
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 terza riga chiede un anno. La quarta spiega perché nessuno glielo concede.
Una risposta che porta un Set-Cookie non è conservabile in una cache condivisa. Un cookie appartiene a un visitatore; conservare quella risposta e darla al successivo significa consegnargli il cookie di un altro. Tutti i CDN si rifiutano. Cloudflare al rifiuto dà un nome, BYPASS; altri semplicemente non conservano l'oggetto e non te lo dicono.
Il cookie era nostro. Un middleware generava un token CSRF a ogni richiesta e lo allegava, così che un form reso in qualsiasi punto del sito ne trovasse uno pronto. In «ogni richiesta» rientrava quella di un font woff2, un file che non disegna form, non legge token e non esegue codice.
La correzione
Il CSRF resta com'è. Quello che smette di succedere è dare un token a chi non può usarlo:
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');
}
Confronta percorsi, non estensioni. Alcune delle risposte che più chiedono la cache non escono dal disco ma da un template, come il gemello in Markdown di ogni pagina o i nostri badge in SVG, e nemmeno quelle disegnano un form.
Se il cookie non l'hai messo tu, guarda il livello di sessione. Parecchi framework aprono una sessione, e ne mandano il cookie, a ogni richiesta che attraversa il middleware, anche quando dentro non ci finisce mai niente.
Diagnosticarlo su qualunque stack
Guarda un asset, non la home. Una home può essere non cacheabile per motivi legittimi, e non dice niente dei trenta file che si porta dietro.
for u in / /css/app.css /js/app.js /fonts/ibm-plex-sans-latin.woff2; do
echo "== $u"
curl -sI "https://esempio.com$u" \
| grep -iE 'cache-control|set-cookie|cf-cache-status|x-cache|^age'
done
Chiedi lo stesso URL due volte. La seconda dovrebbe essere un hit: cf-cache-status: HIT, x-cache: Hit from cloudfront, oppure un header age: che sale su Varnish e su nginx. Se anche la seconda è un miss, l'oggetto non viene conservato, e il motivo è negli header che hai appena stampato.
Gli altri soliti sospetti
Cache-Control: private: solo browser, mai una cache condivisa. no-store: nessuno, nemmeno il browser. E no-cache non vuol dire «non conservare», vuol dire rivalida prima di ogni riuso, che resta un giro completo per file.
Vary: * rende la risposta non cacheabile e basta. Vary: Cookie è quasi altrettanto grave, perché la chiave di cache si porta dentro un valore diverso per ogni visitatore e così non si condivide più niente. Vary: User-Agent frantuma un file in migliaia di varianti. Accept-Encoding è quello che ci sta bene.
Le query string. Quasi tutte le cache usano l'URL intero come chiave, quindi ogni variante con ?utm_source= o ?fbclid= è un oggetto a sé, preso dall'origine. Ripulisci o normalizza i parametri di campagna sull'edge.
E il default del framework, che non arriva al bypass ma costa quasi lo stesso: max-age=0 su ogni file statico sono tante richieste di rivalidazione quanti sono gli asset di ogni navigazione.
Una policy di cache che regge
La regola è quello che promette l'URL, non quello che è il file.
Un URL con un hash del contenuto (app.css?v=9f2ac41b, app.9f2ac41b.css) è un indirizzo di contenuto: se cambia il file, cambia l'URL. Quelli prendono public, max-age=31536000, immutable, e non c'è mai niente da svuotare. Lo stesso file senza il token si ferma a un'ora: il token è la promessa, e non si mette in cache sulla base di una promessa che nessuno ha fatto. Un logo o un'immagine social, che vengono sostituiti sul posto, reggono una settimana.
Resta l'eccezione, quella che quasi tutti sbagliano perché il file sembra la cosa più cacheabile che hanno: qualsiasi cosa incorporata nelle pagine altrui, su un URL che non puoi versionare. Serviamo badge.js con un'ora solo per questo. Gira dentro siti dove non controlliamo né l'HTML né l'URL, quindi un anno di immutable sarebbe un bug irraggiungibile per un anno.
Se servi file con @fastify/static
Due dettagli che ci sono costati un pomeriggio. Manda Cache-Control: public, max-age=0 per impostazione predefinita. E se l'header lo imposti tu con setHeaders, passa anche cacheControl: false, altrimenti la libreria si costruisce il suo a partire dal maxAge di default e sovrascrive il tuo senza dire niente, lasciando qualcosa che sembra voluto:
app.register(fastifyStatic, {
root: join(__dirname, 'public'),
cacheControl: false, // altrimenti setHeaders viene sovrascritto
setHeaders: setStaticCacheHeaders,
});
Prima di toccare una sola impostazione di cache, leggiti una risposta per intero. L'header che rompe la cache quasi mai è quello con «cache» nel nome.
Analizza il tuo sito · Checklist di preparazione all'IA · I bot sono ammessi?