Um cache reutiliza uma resposta que guardou antes para atender uma requisição nova, o que economiza uma ida e volta até a origem e o trabalho de montar a resposta de novo. As regras de cache do HTTP existem porque essa reutilização só é correta sob certas condições, e o erro mais caro nessa área não é uma página desatualizada; é a página de um usuário entregue a outro. As regras estão na RFC 9111 e giram em torno de três perguntas: esta resposta pode ser guardada, a quais requisições futuras ela responde, e ela ainda está fresca.

Caches compartilhados e caches privados

A especificação faz uma distinção antes de qualquer outra. Um cache compartilhado (shared cache) guarda respostas para reutilizar com mais de um usuário; costuma ficar num intermediário, como um reverse proxy ou uma rede de distribuição de conteúdo. Um cache privado (private cache) é dedicado a um único usuário, em geral o do próprio navegador (RFC 9111, seção 1). Uma resposta perfeitamente segura no navegador de uma pessoa pode ser perigosa num cache compartilhado, e várias diretivas existem justamente para dizer que tipo de cache pode guardar o quê.

A quais requisições uma resposta guardada responde

Um cache só pode reutilizar uma resposta guardada para uma requisição com a mesma URI de destino, um método que a resposta guardada permita e, se a resposta nomeou campos de header da requisição no Vary, os mesmos valores nesses campos (RFC 9111, seção 4). O Vary, portanto, restringe a reutilização: uma resposta guardada com Vary: Accept-Language só é reutilizada para requisições posteriores cujo Accept-Language bate com o da requisição que a gerou, e uma resposta com Vary: * nunca corresponde a nada (seção 4.1). A URI mais o que o Vary nomeia é, na prática, a chave do cache.

É aí que o conteúdo personalizado dá errado. Se uma página muda de usuário para usuário por causa de um cookie ou de um token, e a resposta nem proíbe o armazenamento compartilhado nem nomeia o campo que a personaliza, um cache compartilhado vê uma URI e uma resposta guardada, e a entrega à próxima pessoa que pedir. A 9111 é explícita: um header Set-Cookie não impede que a resposta vá para o cache, e com frequência ela é servida pelo cache a requisições posteriores; servidores que querem controlar isso precisam dizer com Cache-Control (seção 7.3). Uma exceção vem embutida: um cache compartilhado não pode reutilizar a resposta a uma requisição que trazia o header Authorization, a menos que a resposta permita explicitamente o armazenamento compartilhado com public, s-maxage ou must-revalidate (seção 3.5).

O que as diretivas dizem de fato

Três diretivas de resposta do Cache-Control são mais confundidas que todas as outras, porque o nome sugere mais do que elas fazem:

  • no-store: o cache não pode guardar nenhuma parte da requisição nem da resposta (seção 5.2.2.5). Esta é a que quer dizer "não guarde isto".
  • no-cache: a resposta pode ser guardada, mas não pode atender uma requisição posterior sem antes ser validada com a origem e receber uma resposta de sucesso (seção 5.2.2.4). Quer dizer "confira antes de reutilizar", não "não faça cache".
  • private: um cache compartilhado não pode guardar a resposta; um cache privado pode (seção 5.2.2.7). É a diretiva para uma página feita para um único usuário.

Uma página personalizada pode, portanto, ir para o cache com segurança de dois jeitos: no privado, no navegador do próprio usuário, marcando-a como private; ou num cache compartilhado com o campo da requisição que a personaliza no Vary, para que cada variante fique guardada sob a sua própria chave. Aumentar ou diminuir o tempo de frescor não muda nenhum desses resultados: uma página guardada sob uma chave ampla demais está errada por um segundo tão certamente quanto por um dia.

Frescor e validação

Uma resposta guardada está fresca enquanto pode ser reutilizada sem perguntar à origem. O tempo de vida vem do max-age, do s-maxage para caches compartilhados, ou do Expires, e quando nenhum deles está presente o cache pode atribuir um tempo heurístico, que a especificação sugere não passar de uma fração do tempo decorrido desde o Last-Modified, tipicamente 10% (seções 4.2.2 e 5.2.2.10). Uma resposta que deixou de estar fresca não é necessariamente descartada: o cache pode validá-la com uma requisição condicional, e a origem confirma que a resposta guardada continua boa ou manda uma nova (seção 4.3). O no-cache apenas torna essa validação obrigatória em toda reutilização.

A conta é curta o bastante para fazer à mão. O cache tira o tempo de vida da primeira destas fontes que se aplicar: o s-maxage, se o cache for compartilhado; depois o max-age; depois o Expires menos o Date (seção 4.2.1). Uma resposta com max-age faz todo cache ignorar o Expires, e uma com s-maxage faz o cache compartilhado ignorá-lo (seção 5.3); o cache privado ignora o próprio s-maxage, que só fala com caches compartilhados (seção 5.2.2.10). A heurística só é permitida quando nenhum deles está presente (seção 4.2.2), e os 10% são um valor típico que a especificação menciona, não uma regra: usá-los ou não é escolha do próprio cache. Um servidor de origem com relógio nunca manda um Last-Modified posterior ao seu Date (RFC 9110, seção 8.8.2.1). O 200 é um dos status definidos como cacheáveis por heurística (RFC 9110, seção 15.1), então uma resposta 200 a um GET pode ser guardada sem Cache-Control nenhum, e o public não acrescenta nada a ela (RFC 9111, seções 3 e 5.2.2.9). A idade atual da resposta parte do maior valor entre a idade aparente (o momento do recebimento menos o Date, nunca abaixo de zero) e o header Age somado ao tempo que a requisição levou; a isso se soma o tempo que ela já passou neste cache (seção 4.2.3). Ela está fresca enquanto o tempo de vida for maior que essa idade (seção 4.2), e o cache que a reutiliza sem validação manda um header Age com a idade atual (seção 4).

Velha não quer dizer inutilizável. O cache precisa validar uma resposta velha antes de reutilizá-la, a menos que esteja desconectado da origem ou tenha permissão explícita para servi-la velha, e nunca a serve velha contra um no-cache, um must-revalidate ou, num cache compartilhado, um s-maxage ou proxy-revalidate (seção 4.2.4). Com must-revalidate, o cache desconectado responde com um erro, que deve ser 504 (Gateway Timeout), em vez da resposta velha (seção 5.2.2.2); o proxy-revalidate diz o mesmo só aos caches compartilhados, e o s-maxage traz essa regra junto (seções 5.2.2.8 e 5.2.2.10). Ao validar, o cache deve mandar o Last-Modified guardado como If-Modified-Since (seção 4.3.1). Os nomes das diretivas são comparados sem diferenciar maiúsculas de minúsculas, e o cache ignora as diretivas que não reconhece (seções 5.2 e 5.2.3); quando uma diretiva aparece duas vezes, a especificação deixa o cache usar a primeira ou tratar a resposta como velha, e incentiva tratar como velha uma informação de frescor inválida, como um max-age que não é um número inteiro (seção 4.2.1). Um número de segundos grande demais para ser representado vale 2147483648 (seção 1.2.2).

Laboratório interativo

Decida se um cache pode reutilizar uma resposta

Uma resposta 200 a um GET está guardada num cache. Defina os headers dela, o tipo de cache e há quanto tempo a resposta chegou, e acompanhe o raciocínio do cache: se ele pode guardá-la, por quanto tempo ela fica fresca e por quê, que idade ela tem agora e se ele pode responder sem perguntar à origem. O Exemplo mostra o s-maxage passando por cima do max-age só no cache compartilhado; mude o cache para privado e depois experimente o no-cache.

O que acontece

  1. Guardar Pode guardar: nada proíbe, e o 200 é cacheável por heurística, então não precisa de diretiva para ser guardado.
  2. Tempo de vida 300 s, do max-age.
  3. Idade atual 90 s: uma idade de partida de 30 s (o maior entre a idade aparente, 2 s, e o Age, 30 s), mais 60 s neste cache.
  4. Fresca ou velha Fresca: o tempo de vida, 300 s, é maior que a idade, 90 s, e ainda restam 210 s.
  5. Origem acessível Responde do cache sem falar com a origem, mandando Age: 90.
  6. Origem inacessível Responde do cache: uma resposta fresca não precisa de contato.

O public não muda nada aqui: a resposta podia ser guardada sem ele.

O que o modelo deixa de fora: diretivas de requisição como max-stale, o Vary e a chave do cache, requisições com Authorization, status diferentes de 200, o tempo que a requisição levou (o atraso da resposta é tomado como zero), ETag, must-understand e extensões como stale-while-revalidate, que este cache não reconhece e por isso ignora; a heurística, quando escolhida, é 10% do tempo entre o Last-Modified e o Date, arredondado para baixo.

Como ler um problema de cache

A maioria dos incidentes de cache se resume às três perguntas, nessa ordem. A resposta podia ser guardada onde foi encontrada, considerando private, no-store e Authorization? Ela foi escolhida para uma requisição que não devia atender, porque a chave, a URI mais o Vary, deixou de fora o que tornava o conteúdo diferente? E foi reutilizada sem validação quando estava velha ou marcada com no-cache? O decodificador de mensagens HTTP mostra os campos que uma resposta carrega, e Cache no NGINX: o que é armazenado, o que é servido mostra como um cache compartilhado muito usado monta a sua chave e aplica as mesmas diretivas.

Fontes