Fundamentos de Programação/03 - Sistemas Reais/Semana 10 - Rede e HTTP7 min
Status Codes
- O que cada família (2xx, 3xx, 4xx, 5xx) comunica sobre de quem é a culpa?
- 401 vs 403: qual é "não sei quem você é" e qual é "sei e você não pode"?
- 400 vs 422: quando o corpo é inválido e quando a requisição é malformada?
- Por que devolver 200 com
{"error": ...}no corpo quebra clientes e caches?
Conceito
O modelo mental mais útil: a família diz de quem é a culpa.
| Família | Significa | Culpa |
|---|---|---|
| 2xx | deu certo | — |
| 3xx | procure em outro lugar | — |
| 4xx | você mandou algo errado | do cliente |
| 5xx | eu falhei processando | do servidor |
Essa divisão não é estética: monitoramento, alarme e lógica de retry se apoiam nela. 4xx em massa é um cliente quebrado; 5xx em massa acorda alguém de madrugada. Trocar um pelo outro faz o alarme apontar para o time errado.
401 vs 403 — a distinção mais errada de todas:
| Significa | O cliente deve | |
|---|---|---|
| 401 Unauthorized | "não sei quem você é" — credencial ausente, inválida ou expirada | autenticar (ou renovar o token) |
| 403 Forbidden | "sei quem você é, e você não pode" | nada — tentar de novo não ajuda |
O nome do 401 é historicamente infeliz: ele significa unauthenticated. A regra prática: se autenticar de novo pode resolver, é 401. Se não pode, é 403.
400 vs 422:
| Quando | |
|---|---|
| 400 Bad Request | não consegui entender — JSON malformado, parâmetro do tipo errado |
| 422 Unprocessable | entendi perfeitamente e é inválido — CPF com dígito errado, data no passado |
O 400 é sobre sintaxe; o 422 é sobre semântica. Um cliente que recebe 400 tem um bug; um que recebe 422 recebeu dado ruim do usuário. São ações diferentes.
{"error": ...} no corpo quebra tudoÉ a decisão que parece conveniente e destrói um sinal legível por máquina:
- caches guardam o erro como se fosse resposta válida
- lógica de retry não repete (200 é sucesso)
- monitoramento mostra 100% de sucesso enquanto tudo falha
- balanceadores não removem a instância doente
- todo cliente é obrigado a parsear o corpo para saber se deu certo
Você tinha um sinal de um byte que qualquer intermediário entende, e trocou por um contrato que só o seu cliente conhece.
Os outros que valem conhecer:
| Código | Uso |
|---|---|
201 Created + Location |
criou; o header aponta para o recurso |
| 202 Accepted | aceitei, vou processar depois (assíncrono) |
| 204 No Content | deu certo, não há corpo (típico de DELETE) |
| 304 Not Modified | seu cache está válido; não mando o corpo |
| 409 Conflict | conflito de estado (duplicata, versão desatualizada) |
| 410 Gone | existia e não existe mais — permanente, ao contrário do 404 |
429 Too Many Requests + Retry-After |
você excedeu o limite; volte em N segundos |
503 Service Unavailable + Retry-After |
indisponível temporariamente |
429 e 503 sem Retry-After transferem para o cliente a decisão de quando voltar — e
ele vai escolher errado.
Em Go
As constantes existem e devem ser usadas: http.StatusNotFound em vez de 404. Erro de
digitação em número passa pelo compilador; nome errado não.
net/http: escrever o corpo envia 200fmt.Fprint(w, "erro") // <- isto JÁ ENVIOU 200
w.WriteHeader(http.StatusBadRequest) // ignorado + aviso no log
A primeira escrita no ResponseWriter envia a linha de status e os headers. Depois
disso, WriteHeader é ignorado e o Go registra
http: superfluous response.WriteHeader call.
A ordem obrigatória é sempre: headers → WriteHeader → corpo.
E o mesmo vale para w.Header().Set(...): depois da primeira escrita, não tem efeito
(6. Headers HTTP).
http.Error(w, msg, código) faz o certo na ordem certa: define
Content-Type: text/plain, chama WriteHeader e escreve a mensagem. Para API JSON você
precisa do seu próprio, porque http.Error manda texto puro.
Um formato de erro, documentado, para toda a API — é um requisito do Semana 11-12 - Projeto Integrador:
type ErroAPI struct {
Codigo string `json:"codigo"` // legível por máquina: "cpf_invalido"
Mensagem string `json:"mensagem"` // legível por humano
Campo string `json:"campo,omitempty"`
}
func responderErro(w http.ResponseWriter, status int, e ErroAPI) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status) // ANTES do corpo
json.NewEncoder(w).Encode(e)
}
Mapear erro de domínio para status num lugar só, e não em cada handler:
func status(err error) int {
switch {
case errors.Is(err, ErrNaoEncontrado): return http.StatusNotFound
case errors.Is(err, ErrJaExiste): return http.StatusConflict
case errors.Is(err, ErrInvalido): return http.StatusUnprocessableEntity
case errors.Is(err, ErrSemPermissao): return http.StatusForbidden
default: return http.StatusInternalServerError
}
}
Isso mantém o domínio livre de HTTP (3. Dependency Inversion Principle) e o mapeamento auditável.
E sql.ErrNoRows merece atenção especial: ele significa "não encontrado", que é 404 —
não 500. Tratá-lo como erro genérico é a causa mais comum de 500 onde deveria haver 404
(2. Agregações).
Respostas às perguntas-guia
1. O que cada família (2xx, 3xx, 4xx, 5xx) comunica sobre de quem é a culpa?
2xx sucesso, 3xx redirecionamento, 4xx culpa do cliente, 5xx culpa do servidor. Monitoramento e retry dependem dessa divisão.
2. 401 vs 403: qual é "não sei quem você é" e qual é "sei e você não pode"?
401 = não autenticado (autenticar pode resolver). 403 = autenticado e sem permissão (não resolve). O nome do 401 é enganoso.
3. 400 vs 422: quando o corpo é inválido e quando a requisição é malformada?
400 = não consegui parsear (sintaxe). 422 = parseei e o conteúdo é inválido (semântica).
Em Go: erro de json.Decoder → 400. Erro da sua validação de domínio → 422.
4. Por que devolver 200 com {"error": ...} no corpo quebra clientes e caches?
Porque destrói o sinal que intermediários entendem: cache guarda o erro, retry não repete, monitoramento mostra sucesso, e todo cliente precisa parsear o corpo para saber o resultado.
Trade-offs
Do conceito:
- Usar códigos precisos compra comportamento correto de intermediários e cobra disciplina.
- 404 em vez de 403 para recurso alheio compra segurança (não revela existência) e cobra diagnóstico confuso para o usuário legítimo.
- 422 é preciso e não é padrão HTTP puro (vem do WebDAV) — alguns clientes o tratam como desconhecido.
Em Go:
http.Errorcompra corretude de ordem e cobra otext/plain.- Mapeamento centralizado de erro→status compra consistência e cobra um lugar a mais para manter.
- A ordem obrigatória headers→status→corpo é uma pegadinha real, e o aviso de
superfluous WriteHeaderé a única pista.
Exemplo prático
package main
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
)
var (
ErrNaoEncontrado = errors.New("não encontrado")
ErrJaExiste = errors.New("já existe")
ErrInvalido = errors.New("dado inválido")
ErrSemPermissao = errors.New("sem permissão")
)
type ErroAPI struct {
Codigo string `json:"codigo"`
Mensagem string `json:"mensagem"`
Campo string `json:"campo,omitempty"`
}
func statusDe(err error) int {
switch {
case errors.Is(err, ErrNaoEncontrado): return http.StatusNotFound
case errors.Is(err, ErrJaExiste): return http.StatusConflict
case errors.Is(err, ErrInvalido): return http.StatusUnprocessableEntity
case errors.Is(err, ErrSemPermissao): return http.StatusForbidden
default: return http.StatusInternalServerError
}
}
func responderErro(w http.ResponseWriter, status int, e ErroAPI) {
w.Header().Set("Content-Type", "application/json") // 1. headers
w.WriteHeader(status) // 2. status
json.NewEncoder(w).Encode(e) // 3. corpo
}
func handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
w.Header().Set("WWW-Authenticate", `Bearer realm="api"`)
responderErro(w, http.StatusUnauthorized,
ErroAPI{"nao_autenticado", "credencial ausente", ""})
return // 401: autenticar PODE resolver
}
if token != "Bearer admin" {
responderErro(w, http.StatusForbidden,
ErroAPI{"sem_permissao", "seu papel não permite esta operação", ""})
return // 403: autenticar de novo NÃO resolve
}
var corpo struct {
CPF string `json:"cpf"`
Idade int `json:"idade"`
}
if err := json.NewDecoder(r.Body).Decode(&corpo); err != nil {
responderErro(w, http.StatusBadRequest,
ErroAPI{"json_invalido", "não consegui interpretar o corpo", ""})
return // 400: SINTAXE
}
if len(corpo.CPF) != 11 {
responderErro(w, http.StatusUnprocessableEntity,
ErroAPI{"cpf_invalido", "cpf precisa de 11 dígitos", "cpf"})
return // 422: entendi, e é inválido
}
if corpo.Idade < 18 {
responderErro(w, http.StatusUnprocessableEntity,
ErroAPI{"menor_de_idade", "idade mínima 18", "idade"})
return
}
w.Header().Set("Location", "/clientes/9")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]any{"id": 9})
}
// A ARMADILHA: escrever o corpo antes do status
func handlerErrado(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "algo deu errado") // JÁ ENVIOU 200
w.WriteHeader(http.StatusBadRequest) // ignorado + log de superfluous
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /clientes", handler)
mux.HandleFunc("GET /errado", handlerErrado)
srv := httptest.NewServer(mux)
defer srv.Close()
casos := []struct{ nome, metodo, path, corpo, auth string }{
{"sem credencial", "POST", "/clientes", `{}`, ""},
{"credencial insuficiente", "POST", "/clientes", `{}`, "Bearer user"},
{"json malformado", "POST", "/clientes", `{cpf`, "Bearer admin"},
{"cpf inválido", "POST", "/clientes", `{"cpf":"123","idade":30}`, "Bearer admin"},
{"menor de idade", "POST", "/clientes", `{"cpf":"12345678901","idade":15}`, "Bearer admin"},
{"tudo certo", "POST", "/clientes", `{"cpf":"12345678901","idade":30}`, "Bearer admin"},
}
for _, c := range casos {
req, _ := http.NewRequest(c.metodo, srv.URL+c.path, strings.NewReader(c.corpo))
if c.auth != "" { req.Header.Set("Authorization", c.auth) }
resp, _ := http.DefaultClient.Do(req)
b := make([]byte, 200); n, _ := resp.Body.Read(b); resp.Body.Close()
fmt.Printf("%-26s -> %d %-8s %s\n", c.nome, resp.StatusCode,
resp.Header.Get("Location"), strings.TrimSpace(string(b[:n])))
}
fmt.Println("\n--- a armadilha: corpo antes do status ---")
r, _ := http.Get(srv.URL + "/errado")
b := make([]byte, 60); n, _ := r.Body.Read(b); r.Body.Close()
fmt.Printf("esperava 400, veio %d (%q)\n", r.StatusCode, strings.TrimSpace(string(b[:n])))
}
A última linha é a demonstração: o handler pediu 400 e o cliente recebeu 200, porque o corpo foi escrito antes.
Relacionado
- 4. Métodos HTTP — o método promete, o status responde
- 6. Headers HTTP —
Location,WWW-Authenticate,Retry-Afteracompanham o status - 3. Dependency Inversion Principle — semana 6, mapear erro de domínio para HTTP numa borda só
- 2. Agregações — semana 9,
sql.ErrNoRowsé 404, não 500
Parte de Semana 10 - Rede e HTTP · 00 - MOC Fundamentos de Programação