# ADR-022 — Cache de resposta, ETag e CSRF fora do HTML cacheável

| Campo | Valor |
|---|---|
| **Status** | Aceito (2026-10-20) |
| **Data** | 2026-10-14 |
| **Plano de origem** | `wiki/plan/18-otimizacao-resposta-hardening.md` |
| **Relacionadas** | ADR-008, ADR-010, ADR-020, ADR-021 |

## Contexto

O `v6-hardening` (Laravel 5.5 / PHP 7.2) mediu que os maiores ganhos vieram do **lado da origem**:
cache de resposta (FPC) + ETag/304 + cache de layout → **TTFB −65%**, **origem −74%**, **LCP −56%**
(relatório da Fase 1). Mas dois obstáculos apareceram e **quebraram o cache**:

1. **CSRF dentro do HTML**: todo layout embutia `<meta name="csrf-token">`/`_token`; cada GET
   anônimo abria nova sessão → o corpo variava por request → `ETag` instável, `304` nunca disparava,
   e cachear `public` vazaria o token de uma sessão para outra (**CSRF 419**).
2. **CSP por nonce por request dentro do HTML**: mesmo efeito no `ETag` e, num HTML cacheado na
   borda, cria a *static nonce vulnerability* (o nonce vaza e o CSP é derrotado). O v6 acabou
   **desligando o CSP** para destravar o cache e manteve apenas headers estáticos.

O v7 é **greenfield** (sem jQuery, sem handlers inline, sem CDN, JS em módulos Vite) — o que
permite resolver os dois problemas **na origem** em vez de contorná-los.

## Decisão

1. **`Cache-Control` para GET público de conteúdo**: `public, max-age, s-maxage,
   stale-while-revalidate`; **`Vary: Accept-Encoding` apenas** (nunca `User-Agent`). Middleware fino.
2. **`ETag` + `304`** nesses GET; o corpo **precisa ser determinístico por request**.
3. **CSRF fora do HTML cacheável**: página com formulário = `private, no-cache`; só páginas de
   conteúdo (sem formulário) são `public`. (Alternativa, se necessário: token buscado no cliente via
   `X-CSRF-TOKEN`/fetch — padrão L1.0 do v6.)
4. **CSP sem inline** (greenfield): nenhum `<script>`/`<style>`/handler inline, zero 3rd-party →
   política **estática e cacheável**, `ETag` estável. **Nunca cachear HTML que embuta nonce por
   request** (evita *static nonce vulnerability*). Se um nonce for inevitável, aquela página não é
   cacheada na borda.
5. **Assets imutáveis**: saída do Vite 8 com hash + `Cache-Control: public, max-age=31536000,
   immutable` (ADR-008).
6. **FPC opcional** (Valkey) para GET público, invalidado por evento de publicação — middleware
   nativo, **sem pacote**. Descartável se o ganho não justificar.
7. **Sem minificação de HTML em runtime** (frágil — o v6 teve bug com comentários `//`); confiar em
   **brotli/gzip** (ADR-008).

## Justificativa (alternativas descartadas)

- **`spatie/laravel-responsecache`** — descartada: dependência; um middleware fino + Valkey cobre.
- **Nonce por request no HTML + borda** — descartada: *static nonce vulnerability* + quebra o `ETag`.
- **CSP por hash no legado** — descartada (lição do v6): handlers `onclick=` e libs que injetam
  `<script>` em runtime inviabilizam hash. No v7 isso não existe (sem inline).
- **Minificar HTML no runtime** — descartada: frágil; gzip/brotli entrega o essencial.

## Consequências

**Positivas**
- Cache de resposta + `304` sem vazar CSRF e sem vulnerabilizar o CSP; TTFB/LCP caem.
- CSP cacheável e simples (sem inline) — seguro por construção.

**Custos / negativas**
- Páginas com formulário não são cacheadas publicamente (aceitável — formulários são poucos).
- FPC exige invalidação por evento (se adotado).

## Exemplo mínimo (comparativo)

```php
// ❌ todo layout com token + nonce por request → corpo varia, 304 nunca dispara, cache inseguro
<meta name="csrf-token" content="{{ csrf_token() }}">
<script nonce="{{ $nonce }}">…</script>

// ✅ conteúdo público determinístico + form pages private; CSP sem inline; assets imutáveis
// middleware: GET portal sem formulário →
//   Cache-Control: public, max-age=300, s-maxage=300, stale-while-revalidate=86400
//   ETag: "<hash do corpo>"  →  304 em If-None-Match
// assets: Cache-Control: public, max-age=31536000, immutable  (hash do Vite)
```

## Verificação

- `curl -I /` (conteúdo público) → `Cache-Control: public…`, `ETag`, `Vary: Accept-Encoding`;
  `If-None-Match` → `304`.
- Página com formulário → `Cache-Control: private, no-cache`; nenhum token em HTML público.
- CSP emitido **sem** `'nonce-'`/`'unsafe-inline'` e **sem** inline no HTML; `ETag` estável entre
  requests idênticos.
- Assets do Vite → `immutable`; `content-encoding: br|gzip`.

## Referências

- `v6-hardening`: `plan/2026-08-09-infra-update.epic.md`, `plan/2026-09-10-csp-enforce-edge-coexistence.md`,
  `wiki/infra-update/{melhorias-cache-csrf-l1-l1.0,correcao-cache-control-borda-l1,full-page-cache-l2-3}.md`;
  `pesquisa-infra-cdn/FASE1_RELATORIO_FINAL.md`.
