Fundamentos de Programação/03 - Sistemas Reais/Semana 10 - Rede e HTTP7 min
Headers HTTP
- Content-Type vs Accept: quem declara e quem pede?
- Como Cache-Control, ETag e If-None-Match cooperam para evitar tráfego?
- Que headers carregam autenticação, e por que não colocar token na URL?
- Quais headers o CORS realmente usa, e por que o preflight existe?
Conceito
Headers são metadados sobre a mensagem — não sobre o recurso. Agrupados por função:
| Categoria | Exemplos |
|---|---|
| representação | Content-Type, Content-Length, Content-Encoding |
| negociação | Accept, Accept-Language, Accept-Encoding |
| cache | Cache-Control, ETag, Last-Modified, Vary |
| condicional | If-None-Match, If-Modified-Since, If-Match |
| autenticação | Authorization, WWW-Authenticate |
| conexão | Connection, Keep-Alive, Transfer-Encoding |
| CORS | Origin, Access-Control-* |
Content-Type vs Accept: quem declara e quem pede.
Content-Typedescreve o corpo desta mensagem. Vai na requisição (descrevendo o que você mandou) e na resposta (descrevendo o que veio).Accepté o cliente dizendo o que ele quer de volta. Só faz sentido na requisição.
Confundir os dois produz o bug de mandar Accept: application/json num POST e esperar que o
servidor entenda que o corpo é JSON.
Cache: como Cache-Control, ETag e If-None-Match cooperam. São dois mecanismos
distintos:
- frescor (
Cache-Control: max-age=60) — por 60 segundos, use a cópia local sem perguntar. Zero requisições. - revalidação (
ETag+If-None-Match) — quando expirou, pergunte "ainda vale este ETag?". Se sim, o servidor responde 304 sem corpo.
O primeiro elimina a requisição; o segundo elimina apenas o corpo. Para uma resposta de 2 MB, o 304 economiza 2 MB e paga um RTT.
Vary é o header que quase todo mundo esquece: ele diz de quais headers da requisição
a resposta depende. Sem Vary: Accept-Encoding, um cache pode entregar a versão gzip para um
cliente que não aceita gzip. Sem Vary: Authorization, pode entregar dados de um usuário para
outro — o pior bug de cache que existe.
Por que não colocar token na URL: URLs são registradas em log de servidor, log de proxy,
histórico do navegador, e vazam no header Referer para terceiros. Além de aparecerem em
bookmarks e em prints de tela. Credencial vai em Authorization ou em cookie — nunca em query
string.
CORS não é segurança do servidor — é uma restrição do navegador. O servidor sempre processa a requisição; o navegador é que decide se entrega a resposta ao JavaScript. Um cliente que não é navegador (curl, Go, mobile) ignora CORS completamente.
O preflight OPTIONS existe porque, para requisições "não simples" (método diferente de
GET/POST/HEAD, ou headers customizados), o navegador precisa pedir permissão antes de
enviar — porque enviar já poderia causar efeito colateral.
Na prática
ETag e 304, verificado:
1a requisicao: 200, 14 bytes, ETag="7b226e6f6d65223a"
2a com If-None-Match: 304, 0 bytes <- economizou o corpo inteiro
Preflight de CORS, verificado:
preflight OPTIONS: 204
Access-Control-Allow-Origin: https://app.exemplo.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Max-Age: 600
O Access-Control-Max-Age: 600 diz ao navegador para não repetir o preflight por 10
minutos — sem ele, cada requisição não-simples custa duas viagens
(2. Ciclo de uma Requisição).
Em Go
w.Write([]byte("ok")) // envia status + HEADERS agora
w.Header().Set("Content-Type", "text/csv") // silenciosamente ignorado
A primeira escrita descarrega a linha de status e os headers. Depois disso, mexer em
w.Header() altera um mapa que ninguém vai ler.
Sem erro, sem aviso — ao contrário do WriteHeader duplicado, que pelo menos registra
superfluous response.WriteHeader call (5. Status Codes).
A ordem é sempre: Header().Set → WriteHeader → Write.
Chaves são canonicalizadas. w.Header().Set("content-type", ...) grava como
Content-Type. Se você precisa de uma chave com capitalização exata (API legada exigente),
tem que escrever direto no mapa:
w.Header()["X-my-Header"] = []string{"valor"} // ignora a canonicalização
Header multivalor: Get devolve só o primeiro. Para todos, Values:
r.Header.Get("Accept") // primeiro valor
r.Header.Values("Accept") // todos (Go 1.14+)
Content-Type é adivinhado se você não definir. net/http sniffa os primeiros 512 bytes
(http.DetectContentType). Para JSON, ele frequentemente adivinha text/plain — então
sempre defina explicitamente.
Gzip: net/http descomprime a resposta automaticamente no cliente (e remove
Content-Encoding), mas não comprime no servidor. Compressão de resposta é middleware
seu, ou do proxy reverso.
Cookies: http.SetCookie(w, &http.Cookie{...}) com HttpOnly, Secure e SameSite
(7. Sessão vs JWT).
Respostas às perguntas-guia
1. Content-Type vs Accept: quem declara e quem pede?
Content-Type declara o formato do corpo desta mensagem. Accept pede o formato da
resposta. O primeiro descreve o presente; o segundo, o futuro.
Em Go: se você não definir Content-Type na resposta, Go adivinha pelos primeiros 512
bytes — e adivinha errado para JSON.
2. Como Cache-Control, ETag e If-None-Match cooperam para evitar tráfego?
Cache-Control define por quanto tempo nem perguntar. Depois disso, If-None-Match com o
ETag guardado pergunta "mudou?", e o servidor responde 304 sem corpo. O primeiro elimina
a viagem; o segundo, o corpo.
Verificado: 200 com 14 bytes → 304 com 0 bytes.
3. Que headers carregam autenticação, e por que não colocar token na URL?
Authorization (e Cookie). Não na URL porque URLs vão para log de servidor, log de proxy,
histórico, Referer e bookmarks.
Em Go: r.Header.Get("Authorization"), e cuidado ao logar r.URL.String() — se alguém
colocou token na query, você acabou de gravá-lo em disco.
4. Quais headers o CORS realmente usa, e por que o preflight existe?
Requisição: Origin, e no preflight Access-Control-Request-Method /
-Headers. Resposta: Access-Control-Allow-Origin / -Methods / -Headers /
-Credentials / -Max-Age.
O preflight existe porque, para requisições não simples, o navegador precisa autorização antes de enviar — o envio em si já poderia causar efeito. Verificado: 204 com os três headers de permissão.
Trade-offs
Do conceito:
max-agelongo compra menos requisições e cobra servir conteúdo velho (sem forma de invalidar antes do prazo).ETagcompra revalidação exata e cobra um RTT mais o custo de calcular o ETag.Varycompra correção de cache e cobra taxa de acerto (mais dimensões, menos reuso).- CORS permissivo (
*) compra conveniência e cobra que qualquer origem chame sua API pelo navegador.
Em Go:
- A ordem obrigatória headers→status→corpo é uma pegadinha silenciosa.
- Canonicalização de chaves compra consistência e cobra um escape para APIs exigentes.
- Descompressão automática no cliente é conveniente e assimétrica (não há compressão automática no servidor).
Exemplo prático
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/http/httptest"
)
func main() {
corpo := []byte(`{"nome":"ana"}`)
soma := sha256.Sum256(corpo)
etag := fmt.Sprintf("%q", hex.EncodeToString(soma[:])[:16])
mux := http.NewServeMux()
// ETag + revalidação condicional
mux.HandleFunc("GET /recurso", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("ETag", etag)
w.Header().Set("Cache-Control", "max-age=60")
w.Header().Set("Vary", "Accept-Encoding") // de que a resposta DEPENDE
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified) // 304: SEM corpo
return
}
w.Header().Set("Content-Type", "application/json") // ANTES de escrever
w.Write(corpo)
})
// CORS com preflight
cors := func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://app.exemplo.com")
w.Header().Set("Vary", "Origin")
if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
w.Header().Set("Access-Control-Max-Age", "600") // não repita o preflight
w.WriteHeader(http.StatusNoContent)
return
}
next(w, r)
}
}
mux.HandleFunc("/api", cors(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "dados")
}))
// A ARMADILHA: header depois da escrita
mux.HandleFunc("GET /tarde", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("corpo"))
w.Header().Set("X-Importante", "perdido") // ignorado, sem aviso
})
// Content-Type adivinhado quando você não define
mux.HandleFunc("GET /adivinhado", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"json":true}`)) // não definimos Content-Type
})
srv := httptest.NewServer(mux)
defer srv.Close()
// 1. cache condicional
r1, _ := http.Get(srv.URL + "/recurso")
b1, _ := io.ReadAll(r1.Body); r1.Body.Close()
fmt.Printf("1a: %d %d bytes ETag=%s Vary=%s\n",
r1.StatusCode, len(b1), r1.Header.Get("ETag"), r1.Header.Get("Vary"))
req, _ := http.NewRequest("GET", srv.URL+"/recurso", nil)
req.Header.Set("If-None-Match", etag)
r2, _ := http.DefaultClient.Do(req)
b2, _ := io.ReadAll(r2.Body); r2.Body.Close()
fmt.Printf("2a: %d %d bytes <- corpo inteiro economizado\n", r2.StatusCode, len(b2))
// 2. preflight
pre, _ := http.NewRequest(http.MethodOptions, srv.URL+"/api", nil)
pre.Header.Set("Origin", "https://app.exemplo.com")
pre.Header.Set("Access-Control-Request-Method", "DELETE")
rp, _ := http.DefaultClient.Do(pre); rp.Body.Close()
fmt.Printf("\npreflight: %d\n", rp.StatusCode)
for _, k := range []string{"Access-Control-Allow-Origin", "Access-Control-Allow-Methods", "Access-Control-Max-Age"} {
fmt.Printf(" %-32s %s\n", k+":", rp.Header.Get(k))
}
// 3. header tarde demais
r3, _ := http.Get(srv.URL + "/tarde"); r3.Body.Close()
fmt.Printf("\nheader definido após Write: %q (vazio = perdido)\n", r3.Header.Get("X-Importante"))
// 4. Content-Type adivinhado
r4, _ := http.Get(srv.URL + "/adivinhado"); r4.Body.Close()
fmt.Printf("Content-Type adivinhado para JSON: %q <- defina sempre\n",
r4.Header.Get("Content-Type"))
// 5. multivalor
req5, _ := http.NewRequest("GET", srv.URL+"/recurso", nil)
req5.Header.Add("Accept", "application/json")
req5.Header.Add("Accept", "text/html")
fmt.Printf("\nGet devolve %q | Values devolve %v\n",
req5.Header.Get("Accept"), req5.Header.Values("Accept"))
}
Relacionado
- 5. Status Codes — 304 e 204 dependem dos headers condicionais
- 4. Métodos HTTP — o preflight existe por causa dos métodos não simples
- 2. Ciclo de uma Requisição — cache elimina o ciclo inteiro
- 7. Sessão vs JWT —
Authorization,Cookie,Set-Cookie
Parte de Semana 10 - Rede e HTTP · 00 - MOC Fundamentos de Programação