Fundamentos de Programação/03 - Sistemas Reais/Semana 10 - Rede e HTTP7 min
Métodos HTTP
- GET, POST, PUT, PATCH, DELETE: qual a semântica prometida de cada um?
- Qual a diferença entre seguro (safe) e idempotente? (ver Idempotent Operations)
- PUT vs PATCH: qual substitui e qual altera parcialmente?
- Por que POST que apaga registro funciona e ainda assim está errado?
Conceito
O método HTTP é uma promessa sobre a semântica da operação — não uma restrição técnica.
Nada impede um GET de apagar dados; o que existe é um contrato que muita coisa no
caminho leva a sério.
As duas propriedades, que são diferentes e confundidas:
| Propriedade | Significa |
|---|---|
| seguro (safe) | não modifica nada no servidor |
| idempotente | N requisições idênticas têm o mesmo efeito que 1 |
Todo método seguro é idempotente (não mudar nada N vezes é o mesmo que não mudar nada uma
vez). O inverso é falso: DELETE é idempotente e não é seguro.
| Método | Seguro | Idempotente | Semântica |
|---|---|---|---|
GET |
sim | sim | ler |
HEAD |
sim | sim | ler só os headers |
OPTIONS |
sim | sim | descobrir capacidades |
PUT |
não | sim | substituir o recurso inteiro |
DELETE |
não | sim | remover |
POST |
não | não | criar, ou "operação sem semântica melhor" |
PATCH |
não | não (em geral) | modificar parcialmente |
Porque software no caminho age sobre elas, sem te consultar:
- caches guardam respostas de
GET— umGETque apaga será apagado pelo cache e pelo crawler - navegadores pré-buscam links (
GET) - proxies e balanceadores repetem requisições idempotentes automaticamente
- bibliotecas de retry repetem
GET/PUT/DELETEsem perguntar, e não repetemPOST Retry-Aftere circuit breakers dependem da idempotência para decidir
Um POST que apaga registro funciona e continua errado: você quebrou o contrato do
qual outros programas dependem.
PUT vs PATCH:
PUT /pedidos/1com{"status":"pago"}substitui o recurso inteiro — os campos ausentes deveriam ser removidos ou voltar ao default. Idempotente.PATCH /pedidos/1com{"status":"pago"}altera só aquele campo. Não é idempotente em geral (um patch do tipo "incremente 1" claramente não é).
Metade das APIs usa PUT com semântica de PATCH. Funciona, e cria a expectativa errada em
quem integra.
POST e a idempotência que você precisa mesmo assim: para pagamento, POST duplicado é
cobrança dupla. A solução padrão é a chave de idempotência — um header
Idempotency-Key: <uuid> que o servidor registra; a segunda requisição com a mesma chave
devolve o resultado da primeira em vez de executar de novo.
Em Go
Go 1.22 mudou o roteamento da stdlib e tornou roteador externo dispensável na maioria dos casos:
mux := http.NewServeMux()
mux.HandleFunc("GET /pedidos/{id}", buscar)
mux.HandleFunc("PUT /pedidos/{id}", substituir)
mux.HandleFunc("PATCH /pedidos/{id}", alterar)
mux.HandleFunc("DELETE /pedidos/{id}", remover)
mux.HandleFunc("POST /pedidos", criar)
Antes disso você escrevia switch r.Method em cada handler, ou usava chi/gorilla/mux.
Três comportamentos do ServeMux que vale conhecer:
- 405 automático — se o caminho casa e o método não, ele responde
405 Method Not AllowedcomAllowpreenchido. Você não escreve nada (5. Status Codes). GETtambém atendeHEAD— registrarGET /xfazHEAD /xfuncionar, com o corpo descartado. É o comportamento correto e gratuito.r.PathValue("id")extrai o wildcard;{caminho...}captura o resto.
Use as constantes, não strings: http.MethodPost em vez de "POST" — erro de digitação
em string passa pelo compilador.
No cliente, http.NewRequestWithContext com o método explícito, e note que http.Get,
http.Post e http.PostForm são atalhos que usam o DefaultClient sem timeout
(2. Ciclo de uma Requisição).
Detalhe de retry em Go: o http.Transport repete automaticamente requisições
idempotentes quando a conexão reusada falha — mas só se o corpo for redigível
(Request.GetBody definido) ou ausente. É a idempotência do protocolo sendo usada pela
stdlib.
Respostas às perguntas-guia
1. GET, POST, PUT, PATCH, DELETE: qual a semântica prometida de cada um?
Ler; criar/operação genérica; substituir inteiro; alterar parcial; remover. A tabela acima com as propriedades.
Em Go: declaradas no padrão de rota do ServeMux, o que documenta a intenção no próprio
roteamento.
2. Qual a diferença entre seguro (safe) e idempotente?
Seguro = não modifica. Idempotente = repetir dá o mesmo resultado. DELETE é idempotente e
não é seguro — apagar duas vezes deixa o mesmo estado final, mas modifica.
Ver Idempotent Operations no roadmap de System Design para o tratamento em nível de sistema distribuído.
3. PUT vs PATCH: qual substitui e qual altera parcialmente?
PUT substitui o recurso inteiro (idempotente). PATCH altera parte (não necessariamente
idempotente).
Em Go: a diferença aparece no unmarshal — para PATCH você precisa distinguir "campo
ausente" de "campo com valor zero", o que exige *string/*int ou
map[string]json.RawMessage. Com string simples, você não sabe se o cliente mandou "" ou
não mandou nada.
4. Por que POST que apaga registro funciona e ainda assim está errado?
Porque funciona por acidente: o protocolo não impede. Está errado porque caches, proxies, crawlers e bibliotecas de retry agem sobre as promessas do método. Você quebrou um contrato do qual outros programas dependem, e o sintoma aparece longe da causa.
Trade-offs
Do conceito:
- Respeitar a semântica compra comportamento correto de caches, retries e intermediários; cobra disciplina e mais rotas.
PUTcompra idempotência e cobra que o cliente envie o recurso inteiro.PATCHcompra payload menor e cobra ambiguidade (JSON Merge Patch, JSON Patch, ou seu próprio formato).POSTcom chave de idempotência compra segurança contra duplicação e cobra guardar as chaves.
Em Go:
ServeMux(1.22+) compra roteamento por método sem dependência e cobra funcionalidades de roteadores maiores.- 405 e
HEADautomáticos vêm de graça e são fáceis de perder ao trocar por roteador externo. - Distinguir ausente de zero em
PATCHcobra ponteiros nos DTOs.
Exemplo prático
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync"
)
type Pedido struct {
ID string `json:"id"`
Status string `json:"status"`
Nota string `json:"nota"`
}
// PATCH precisa distinguir "campo ausente" de "campo vazio": PONTEIROS.
type patchPedido struct {
Status *string `json:"status"`
Nota *string `json:"nota"`
}
func main() {
var mu sync.Mutex
dados := map[string]Pedido{"1": {ID: "1", Status: "rascunho", Nota: "original"}}
idem := map[string]string{} // chave de idempotência -> id criado
mux := http.NewServeMux()
mux.HandleFunc("GET /pedidos/{id}", func(w http.ResponseWriter, r *http.Request) {
mu.Lock(); defer mu.Unlock()
p, ok := dados[r.PathValue("id")]
if !ok { http.Error(w, "não encontrado", http.StatusNotFound); return }
json.NewEncoder(w).Encode(p)
})
// PUT: SUBSTITUI o recurso inteiro. Idempotente.
mux.HandleFunc("PUT /pedidos/{id}", func(w http.ResponseWriter, r *http.Request) {
var novo Pedido
if err := json.NewDecoder(r.Body).Decode(&novo); err != nil {
http.Error(w, "json inválido", http.StatusBadRequest); return
}
mu.Lock(); defer mu.Unlock()
novo.ID = r.PathValue("id")
dados[novo.ID] = novo // campos ausentes ficam ZERADOS: é o contrato do PUT
json.NewEncoder(w).Encode(novo)
})
// PATCH: altera SÓ o que veio. Ponteiro nil = ausente.
mux.HandleFunc("PATCH /pedidos/{id}", func(w http.ResponseWriter, r *http.Request) {
var p patchPedido
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
http.Error(w, "json inválido", http.StatusBadRequest); return
}
mu.Lock(); defer mu.Unlock()
atual, ok := dados[r.PathValue("id")]
if !ok { http.Error(w, "não encontrado", http.StatusNotFound); return }
if p.Status != nil { atual.Status = *p.Status }
if p.Nota != nil { atual.Nota = *p.Nota }
dados[atual.ID] = atual
json.NewEncoder(w).Encode(atual)
})
// POST não é idempotente: chave de idempotência resolve
mux.HandleFunc("POST /pedidos", func(w http.ResponseWriter, r *http.Request) {
chave := r.Header.Get("Idempotency-Key")
mu.Lock(); defer mu.Unlock()
if chave != "" {
if id, ok := idem[chave]; ok { // já processada: devolve o mesmo resultado
w.Header().Set("Location", "/pedidos/"+id)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(dados[id])
return
}
}
id := fmt.Sprint(len(dados) + 1)
p := Pedido{ID: id, Status: "rascunho"}
dados[id] = p
if chave != "" { idem[chave] = id }
w.Header().Set("Location", "/pedidos/"+id)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(p)
})
// DELETE é idempotente: apagar duas vezes deixa o mesmo estado final
mux.HandleFunc("DELETE /pedidos/{id}", func(w http.ResponseWriter, r *http.Request) {
mu.Lock(); defer mu.Unlock()
delete(dados, r.PathValue("id"))
w.WriteHeader(http.StatusNoContent)
})
srv := httptest.NewServer(mux)
defer srv.Close()
chamar := func(metodo, path, corpo string, hdr map[string]string) {
req, _ := http.NewRequest(metodo, srv.URL+path, strings.NewReader(corpo))
for k, v := range hdr { req.Header.Set(k, v) }
resp, _ := http.DefaultClient.Do(req)
b := make([]byte, 200); n, _ := resp.Body.Read(b); resp.Body.Close()
fmt.Printf("%-7s %-14s -> %d %s %s\n", metodo, path, resp.StatusCode,
resp.Header.Get("Location"), strings.TrimSpace(string(b[:n])))
}
fmt.Println("--- PUT substitui o recurso INTEIRO (nota some) ---")
chamar("GET", "/pedidos/1", "", nil)
chamar("PUT", "/pedidos/1", `{"status":"pago"}`, nil)
fmt.Println("\n--- PATCH altera só o que veio (nota preservada) ---")
chamar("PUT", "/pedidos/1", `{"status":"rascunho","nota":"original"}`, nil)
chamar("PATCH", "/pedidos/1", `{"status":"pago"}`, nil)
fmt.Println("\n--- POST NÃO é idempotente: 2 chamadas, 2 recursos ---")
chamar("POST", "/pedidos", "{}", nil)
chamar("POST", "/pedidos", "{}", nil)
fmt.Println("\n--- POST COM chave de idempotência: 2 chamadas, 1 recurso ---")
k := map[string]string{"Idempotency-Key": "abc-123"}
chamar("POST", "/pedidos", "{}", k)
chamar("POST", "/pedidos", "{}", k)
fmt.Println("\n--- DELETE é idempotente: 204 nas duas ---")
chamar("DELETE", "/pedidos/1", "", nil)
chamar("DELETE", "/pedidos/1", "", nil)
fmt.Println("\n--- 405 e HEAD automáticos, sem escrever nada ---")
chamar("POST", "/pedidos/1", "", nil)
chamar("HEAD", "/pedidos/2", "", nil)
}
Relacionado
- 5. Status Codes — o método promete, o status responde
- Idempotent Operations (ref) — idempotência no nível de sistema distribuído
- REST (ref) — os métodos são metade do que define REST
- 6. Headers HTTP —
Allow,Location,Idempotency-Key
Parte de Semana 10 - Rede e HTTP · 00 - MOC Fundamentos de Programação