
# Porque o teu CDN não faz cache de nada

Todos os ficheiros estáticos de agentready.md voltavam com a mesma linha:

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

Tipos de letra, folhas de estilo, scripts, imagens. Não `MISS`, que quereria dizer que a edge ainda não tinha visto o ficheiro e vai guardar a cópia que acabou de ir buscar. `BYPASS` quer dizer que a edge leu a resposta, decidiu que não a podia guardar, e amanhã decide o mesmo. Cada visitante puxava tudo da origem, e teria continuado a fazê-lo para sempre.

## As quatro definições que não eram o problema

- **Development Mode** — desligado. Desactiva por completo a cache da edge durante três horas e fica ligado por esquecimento, por isso é a primeira coisa a ver.
- **Caching Level** — Standard.
- **Page Rules** — nenhuma punha Cache Level em Bypass.
- **Browser Cache TTL** — um valor sensato.

Tudo o que o painel tinha a dizer sobre cache dizia *guarda isto*. A edge não guardava nada.

## A causa estava na resposta, não no painel

Aparece assim que se lê o bloco de cabeçalhos inteiro em vez da única linha que se foi procurar:

```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
```

A terceira linha pede um ano. A quarta explica porque é que ninguém lho dá.

**Uma resposta que traz `Set-Cookie` não pode ser guardada numa cache partilhada.** Um cookie é de um visitante; guardar essa resposta e entregá-la ao seguinte é dar-lhe o cookie de outra pessoa. Todos os CDN recusam. A Cloudflare dá um nome à recusa, `BYPASS`; outros limitam-se a não guardar o objecto, sem dizer nada.

O cookie era nosso. Um middleware gerava um token CSRF em cada pedido e anexava-o, para que qualquer formulário do site encontrasse um à espera. Em «cada pedido» entrava o de um tipo de letra woff2, um ficheiro que não desenha formulários, não lê tokens e não corre código.

## A correcção

O CSRF fica como está. O que deixa de acontecer é dar um token a quem não o pode usar:

```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');
}
```

Compara caminhos, não extensões. Algumas das respostas que mais precisam de cache não saem do disco mas de um template, como o gémeo em Markdown de cada página ou os nossos badges em SVG, e também nunca desenham um formulário.

Se o cookie não foi posto por ti, olha para a camada de sessão. Muitos frameworks abrem sessão, e mandam o cookie dela, em qualquer pedido que passe pelo middleware, mesmo que lá dentro nunca se guarde nada.

## Diagnosticar isto em qualquer stack

Vê um asset, não a página inicial. Uma página inicial pode não ser cacheável por bons motivos, e não diz nada sobre os trinta ficheiros que arrasta atrás.

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

Pede o mesmo URL duas vezes. O segundo pedido devia ser um acerto: `cf-cache-status: HIT`, `x-cache: Hit from cloudfront`, ou um cabeçalho `age:` que sobe em Varnish e no nginx. Se o segundo também falha, o objecto não está a ser guardado, e o motivo está nos cabeçalhos que acabaste de imprimir.

## Os outros suspeitos do costume

`Cache-Control: private`: só navegadores, nunca uma cache partilhada. `no-store`: ninguém, nem o navegador. E `no-cache` não quer dizer «não guardes», quer dizer revalida antes de cada utilização, o que continua a ser uma ida e volta por ficheiro.

`Vary: *` deixa a resposta fora da cache, ponto final. `Vary: Cookie` é quase tão mau, porque a chave de cache passa a incluir um valor diferente por visitante e assim nada é partilhado. `Vary: User-Agent` parte um ficheiro em milhares de variantes. `Accept-Encoding` é o que faz sentido ali.

As query strings. A maioria das caches usa o URL inteiro como chave, por isso cada variante com `?utm_source=` ou `?fbclid=` é um objecto à parte, pedido à origem. Limpa ou normaliza os parâmetros de campanha na edge.

E o valor por omissão do framework, que não chega a ser um bypass mas custa quase o mesmo: `max-age=0` em cada ficheiro estático são tantos pedidos de revalidação quantos os assets de cada navegação.

## Uma política de cache que se aguenta

A regra é o que o URL promete, não o que o ficheiro é.

Um URL com um hash do conteúdo (`app.css?v=9f2ac41b`, `app.9f2ac41b.css`) é um endereço de conteúdo: se o ficheiro muda, o URL muda. Esses levam `public, max-age=31536000, immutable` e nunca há nada para purgar. O mesmo ficheiro sem o token fica por uma hora: o token é a promessa, e nada se guarda em cache com base numa promessa que ninguém fez. Um logótipo ou uma imagem social, substituídos no lugar, aguentam uma semana.

Fica a excepção, aquela em que quase toda a gente falha porque o ficheiro parece o mais cacheável que se tem: o que quer que esteja embebido em páginas alheias, num URL que não podes versionar. Servimos o `badge.js` com uma hora só por isso. Corre dentro de sites onde não controlamos nem o HTML nem o URL, por isso um ano de `immutable` seria um bug fora do nosso alcance durante um ano.

## Se serves ficheiros com @fastify/static

Dois detalhes que nos custaram uma tarde. Manda `Cache-Control: public, max-age=0` por omissão. E se és tu a pôr o cabeçalho com `setHeaders`, passa também `cacheControl: false`, senão a biblioteca constrói o dela a partir do `maxAge` por omissão e escreve por cima do teu, sem avisar, deixando uma coisa com ar de intencional:

```js
app.register(fastifyStatic, {
  root: join(__dirname, 'public'),
  cacheControl: false,           // senão o setHeaders é substituído
  setHeaders: setStaticCacheHeaders,
});
```

Antes de mexer numa única definição de cache, lê uma resposta inteira. O cabeçalho que estraga a cache raramente é o que tem «cache» no nome.

[Analisa o teu site](/pt) · [Checklist de preparação para IA](/pt/tools/ai-readiness-checklist) · [Os bots estão autorizados?](/pt/tools/ai-crawlers-checker)
