Blog

O post despublicado que não morria: por que a API responde 200 e não 404

4 de agosto de 2026 · Time Orion

Encontramos este em produção, no dia em que o blog subiu — e ele é o tipo de bug que só aparece quando você testa o caminho de remoção, que é justamente o caminho que ninguém testa.

O sintoma

Publicamos um post de teste. Ele apareceu no site em segundos, como esperado. Apagamos o post e disparamos a revalidação.

O índice atualizou na hora: o post sumiu da listagem. Perfeito.

Mas a página do post continuou respondendo 200, com o conteúdo inteiro. Na segunda visita, na terceira, depois de aguardar — continuava lá. Um post que não existia mais no banco continuava público na internet.

A causa

O site busca o post numa API interna com cache por tag. Quando o post existe, a API responde 200 com o JSON, e esse resultado entra no cache de dados do framework. Quando o post não existe, a API respondia 404.

E aqui está o detalhe que explica tudo: resposta não-2xx não entra no cache de dados.

É uma decisão sensata do framework — cachear erro é como se transforma uma indisponibilidade momentânea em falha prolongada. Só que a consequência, no nosso caso, era perversa:

  1. A revalidação marca a entrada como obsoleta.
  2. A página é visitada; o framework refaz a busca.
  3. A API responde 404.
  4. O 404 não substitui a entrada obsoleta — não há resultado novo para gravar.
  5. O que o framework tem para servir continua sendo o conteúdo antigo.

O post virou imortal. A entrada nunca era substituída porque a resposta que deveria substituí-la não era cacheável.

A correção

Trocamos o contrato da API. Post inexistente ou despublicado passou a responder:

{ "post": null }

Com status 200 e o mesmo cabeçalho de cache do caminho de sucesso.

Agora o passo 4 funciona: o null é um resultado legítimo, entra no cache, substitui o conteúdo velho — e o site, ao receber null, renderiza a página de "não encontrado" de verdade.

Um post apagado passa a sumir na visita seguinte.

"Mas 404 não é o código semanticamente correto?"

Para uma API pública consumida por humanos e por clientes HTTP genéricos, sim, seria.

Mas essa API tem exatamente um consumidor, e é um consumidor com cache. O contrato correto é o que funciona para o consumidor real — e aqui a ausência do recurso é um estado válido do dado, não uma falha da requisição. A requisição funcionou perfeitamente; a resposta é "não existe".

A distinção é essa: 404 diz "sua requisição falhou"; {post: null} diz "sua requisição funcionou e a resposta é vazia". Para um sistema de cache, a diferença é tudo.

Vale registrar o limite: se um dia essa API virar pública para terceiros, a decisão precisa ser revisitada — provavelmente com dois caminhos.

A lição que generaliza

Toda camada de cache tem uma pergunta que quase ninguém faz: o que acontece com a entrada quando a origem deixa de ter a resposta?

Três respostas possíveis, e só uma costuma estar implementada:

  • a origem responde o vazio, e o vazio é cacheável → a entrada é substituída (correto);
  • a origem responde erro → a entrada obsoleta sobrevive (o nosso bug);
  • a origem não responde → depende da política de stale, e provavelmente serve o velho (aceitável por um tempo, perigoso para sempre).

E a lição operacional: teste o caminho da remoção. Publicar é o caminho feliz e é o que todo mundo testa. Despublicar, apagar, revogar acesso — esses são os caminhos onde o cache guarda o que não deveria, e onde o bug tem consequência de segurança, não só de correção.

O resumo

Quando o vazio é um estado legítimo do dado, ele precisa ser cacheável como qualquer outro. Responder erro para "não existe" faz o cache guardar a última versão boa — para sempre.

Custou uma linha e um comentário explicando por que 200 e não 404. O comentário é a parte importante: sem ele, a próxima pessoa "corrige" de volta.