trilha

Fundamentos de Programação/03 - Sistemas Reais/Semana 10 - Rede e HTTP7 min

Métodos HTTP

Perguntas-guia
  • 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
Por que essas promessas importam de verdade

Porque software no caminho age sobre elas, sem te consultar:

  • caches guardam respostas de GET — um GET que 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/DELETE sem perguntar, e não repetem POST
  • Retry-After e 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/1 com {"status":"pago"} substitui o recurso inteiro — os campos ausentes deveriam ser removidos ou voltar ao default. Idempotente.
  • PATCH /pedidos/1 com {"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:

  1. 405 automático — se o caminho casa e o método não, ele responde 405 Method Not Allowed com Allow preenchido. Você não escreve nada (5. Status Codes).
  2. GET também atende HEAD — registrar GET /x faz HEAD /x funcionar, com o corpo descartado. É o comportamento correto e gratuito.
  3. 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.
  • PUT compra idempotência e cobra que o cliente envie o recurso inteiro.
  • PATCH compra payload menor e cobra ambiguidade (JSON Merge Patch, JSON Patch, ou seu próprio formato).
  • POST com 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 HEAD automáticos vêm de graça e são fáceis de perder ao trocar por roteador externo.
  • Distinguir ausente de zero em PATCH cobra 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


Parte de Semana 10 - Rede e HTTP · 00 - MOC Fundamentos de Programação

Buscar

Busca por título, seção e texto das notas