trilha

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

Headers HTTP

Perguntas-guia
  • 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-Type descreve 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:

  1. frescor (Cache-Control: max-age=60) — por 60 segundos, use a cópia local sem perguntar. Zero requisições.
  2. 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

Header definido depois da primeira escrita não tem efeito
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().SetWriteHeaderWrite.

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-age longo compra menos requisições e cobra servir conteúdo velho (sem forma de invalidar antes do prazo).
  • ETag compra revalidação exata e cobra um RTT mais o custo de calcular o ETag.
  • Vary compra 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


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

Buscar

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