A intenção por trás das cinco classes
O primeiro dígito do status define a classe, e isso é proposital: mesmo sem conhecer um código específico (um 425 novo, por exemplo), o cliente consegue tratá-lo pela classe — todo 4xx significa “não repita do mesmo jeito, o problema está na requisição”, todo 5xx “pode tentar mais tarde”, todo 3xx “olhe o Location”. A RFC 9110 exige explicitamente que o cliente entenda a semântica da classe e permite que ele não entenda o código individual. É também por isso que um status personalizado precisa cair na centena correta.
Por que o 401 se chama Unauthorized e significa “não autenticado”
Um erro histórico de nomenclatura. O HTTP antigo não separava authentication (quem você é) de authorization (o que você pode fazer); o 401 foi batizado de Unauthorized, mas o cabeçalho WWW-Authenticate que o acompanha e a semântica de “pode dar certo se você enviar as credenciais de novo” falam de autenticação. A RFC 9110, na §15.5.2, passou a escrever "lacks valid authentication credentials". Guarde uma frase: 401 = faça login, 403 = mesmo logado não pode. O front-end usa isso para decidir entre redirecionar ao login e mostrar “sem permissão”; invertido, o usuário logado é jogado de volta à tela de login o tempo todo.
Os dois eixos dos redirecionamentos: permanente / temporário, método preservado ou não
Os quatro códigos de redirecionamento comuns formam uma matriz 2×2. 301 e 308 são permanentes (navegador e mecanismos de busca memorizam o novo endereço), 302 e 307 são temporários. Historicamente, ao tratar 301/302 o navegador transformava POST em GET; a RFC 9110 reconhece essa realidade e oferece 307/308 como as versões estritas, que “precisam manter o método e o corpo da requisição”. Para redirecionar um site para https, o 301 serve; ao migrar um endpoint de API, um POST precisa de 307/308, senão o corpo da requisição se perde.
Cache e o 304
O 304 costuma ser entendido como “economizou uma requisição”. Na prática o navegador continua enviando uma requisição condicional com If-None-Match / If-Modified-Since, o servidor compara e responde 304 sem corpo — o que se economiza é a transferência, não a ida e volta. Economizar requisições de verdade é papel do Cache-Control: max-age (dentro da validade o navegador nem pergunta ao servidor); os dois trabalham juntos: durante o max-age usa-se a resposta direto e, depois de expirar, uma requisição condicional renova com 304. No DevTools, "200 (from disk cache)" é o primeiro caso e "304" o segundo.
Os três irmãos do gateway: 502 / 503 / 504
A família 5xx mais comum em produção; distingui-los já reduz o campo de investigação. O proxy reverso (Nginx, gateway, CDN) não consegue conectar ao upstream, ou o upstream devolveu algo inválido — 502, então vá ver se o processo da aplicação caiu. O upstream avisa explicitamente que está ocupado ou em manutenção — 503, normalmente devolvido de propósito, então olhe health checks e circuit breakers. O upstream não responde — 504, então olhe consultas lentas e configuração de timeout. O Cloudflare desdobra esses três casos em 520–526, sendo 521 ≈ 502 (conexão recusada), 522/524 ≈ 504 (timeout de conexão / leitura) e 525/526 problemas na camada TLS.
Status HTTP e código de negócio precisam concordar
O antipadrão mais comum é devolver 200 em toda requisição e colocar o erro num campo code do corpo. Isso faz o CDN cachear páginas de erro, derruba os alertas de 5xx do monitoramento, esvazia o significado de fetch().ok no navegador e deixa qualquer estratégia de retry sem base. O certo é dividir: o status HTTP expressa o que aconteceu na camada da requisição (a semântica da RFC 9110) e o código de negócio do corpo expressa o detalhe do domínio — os dois se complementam em vez de se substituírem: estoque esgotado é 422 + {"code":"OUT_OF_STOCK"}, não 200 + {"code":500}.