Propagação de Cancelamento em Handlers HTTP
Todo handler HTTP deve tratar r.Context() como a raiz da sua árvore de trabalho e passá-lo inalterado para I/O downstream.
Busque em todas as páginas da documentação
Todo handler HTTP deve tratar r.Context() como a raiz da sua árvore de trabalho e passá-lo inalterado para I/O downstream.
Quando o cliente desconecta ou o servidor impõe um timeout, esse contexto é cancelado e os chamados cooperativos devem parar.
net/http anexa um context.Context por requisição a cada *http.Request.
Handlers passam r.Context() para consultas a banco de dados, chamadas HTTP de saída e stubs gRPC para que requisições abandonadas não continuem consumindo recursos.
Wrappers de framework em chi, gin e echo ainda expõem o mesmo contexto de requisição subjacente.
Cartão de receita de referência rápida - pronto para copiar e colar.
func usersHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
users, err := store.ListUsers(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {
return // cliente desconectado; não escreva 500
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(users)
}Quando usar isso:
r.Context() como o primeiro argumento downstream.context.Canceled quando a resposta não puder alcançar o cliente.WithTimeout mais curto apenas em limites internos intencionais, não na raiz do handler.package main
import (
"context"
"database/sql"
"encoding/json"
"errors"
"log"
"net/http"
"time"
_ "github.com/mattn/go-sqlite3"
)
type Store struct{ db *sql.DB }
func (s *Store) SlowQuery(ctx context.Context) (string, error) {
var out string
err := s.db.QueryRowContext(ctx, `SELECT 'ok'`).Scan(&out)
return out, err
}
func handler(store *Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
val, err := store.SlowQuery(ctx)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(map[string]string{"status": val})
}
}
func main() {
db, _ := sql.Open("sqlite3", ":memory:")
defer db.Close()
mux := http.NewServeMux()
mux.Handle("/api", handler(&Store{db: db}))
srv := &http.Server{Addr: ":8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second}
log.Fatal(srv.ListenAndServe())
}O que isso demonstra:
r.Context(), não de context.Background().QueryRowContext respeita o cancelamento quando o cliente aborta.context.Canceled e context.DeadlineExceeded interrompem sem um 500 enganoso.ReadHeaderTimeout em nível de servidor complementa os orçamentos por handler.http.Server cria um contexto de requisição quando aceita uma conexão.ResponseWriter após o cancelamento podem falhar silenciosamente; proteja com verificações de contexto antes de trabalho caro.(w, r) e passar r.Context() para next.ServeHTTP sem substituí-lo, a menos que esteja adicionando valores ou prazos.| Framework | Acessa ctx da requisição | Padrão de Middleware |
|---|---|---|
| net/http | r.Context() | func(next http.Handler) http.Handler |
| chi | r.Context() | middleware.Timeout envolve ctx filho |
| gin | c.Request.Context() | c.Request.WithContext(ctx) para substituir |
| echo | c.Request().Context() | middleware.TimeoutWithConfig |
| Camada | Regra |
|---|---|
| Handler | Comece com r.Context() |
| Serviço | Aceite ctx como primeiro parâmetro |
| SQL | Use QueryContext, ExecContext |
| HTTP de Saída | http.NewRequestWithContext |
| gRPC | Métodos Stub aceitam ctx |
| Goroutines | Passe ctx; pare em Done() |
// Middleware que adiciona um valor de ID de requisição - ainda preserva o cancelamento pai
func requestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := uuid.NewString()
ctx := context.WithValue(r.Context(), requestIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}r.Context().r.Context() ou desanexe explicitamente com context.WithoutCancel apenas para limpeza assíncrona intencional.select em ctx.Done() entre os blocos.defer r.Body.Close() em handlers que leem corpos.| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
Server BaseContext | Valores para todo o processo em todas as requisições | Cancelamento por requisição (use ctx da requisição) |
context.WithoutCancel | Logging de auditoria após a resposta | I/O normal do handler |
Canal done manual | Código legado | Novos handlers HTTP |
| Timeout curto apenas no handler | Proteger uma única consulta lenta | Substituir o contexto inteiro da requisição |
| Fila de workers desacoplada do HTTP | Tarefas assíncronas sobrevivem à requisição | Usuário espera por resultado síncrono |
Na desconexão do cliente, conclusão do handler, ou configurações de timeout do servidor.
O tempo exato depende dos campos de http.Server e do comportamento da camada TLS.
Envolva com WithValue, WithTimeout, ou WithCancel e chame r.WithContext(child).
Nunca substitua context.Background() no meio da cadeia.
Use httptest mais um cliente cancelável ou feche o cliente gravador.
Veja o artigo de testes nesta seção.
c.Set armazena chaves locais do gin; use c.Request.Context() para propagação de cancelamento.
Ele envolve handlers com um contexto de timeout e retorna 503 na expiração.
Ainda passe o contexto envolvido downstream.
Sim - http.NewRequestWithContext(r.Context(), ...) vincula o tempo de vida do cliente às chamadas de dependência.
Registre em nível de debug quando errors.Is(err, context.Canceled) e uma resposta não foi iniciada.
Evite ruído de nível de erro para comportamento normal do cliente.
Apenas com context.WithoutCancel para tarefas intencionais de "disparar e esquecer".
Padrão: pare o trabalho quando a requisição terminar.
Handlers de upgrade ainda começam a partir de r.Context(); o tempo de vida da conexão pode excedê-lo.
Gerencie o cancelamento de WS com sinais de fechamento de conexão separadamente.
Ele envolve r.Context() com um prazo mais curto e cancela quando excedido.
O downstream deve respeitar o ctx envolvido.
Versões de Stack: Esta página foi escrita para Go 1.26.x (padrão GC Green Tea, go fix modernizers - verifique o patch na compilação), chi (última versão - verifique na compilação), gin (última versão - verifique na compilação), echo (última versão - verifique na compilação), google.golang.org/grpc (última versão - verifique na compilação), sigs.k8s.io/controller-runtime (última versão - verifique na compilação), kubebuilder (última versão - verifique na compilação), tinygo (última versão - verifique os alvos de placa na compilação), wazero (última versão - verifique na compilação) e golangci-lint (última versão - verifique o conjunto de linters na compilação).
Revisado por Chris St. John·Última atualização: 18 de jul. de 2026