Control Flow in HTTP Middleware
Los manejadores HTTP se ejecutan concurrentemente bajo carga, por lo que un solo panic no debe bloquear el proceso.
Busca en todas las páginas de la documentación
Los manejadores HTTP se ejecutan concurrentemente bajo carga, por lo que un solo panic no debe bloquear el proceso.
El middleware envuelve los manejadores con defer/recover, da forma al flujo de control específico de la solicitud y decide si llamar al siguiente manejador; patrones compartidos entre chi, gin, echo y el net/http de la librería estándar.
Envuelve cada manejador de solicitud para que un recover diferido registre el panic y devuelva HTTP 500.
Usa defer para temporizadores por solicitud, cierre de cuerpo y spans de rastreo.
El middleware retorna anticipadamente al no llamar a next, o al escribir una respuesta y retornar.
La recuperación de panics pertenece a la capa útil más externa, generalmente el middleware del servidor o del enrutador.
Empareja la recuperación con el registro estructurado y los IDs de solicitud del contexto.
Tarjeta de receta de referencia rápida, lista para copiar y pegar.
package main
import (
"log"
"net/http"
)
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("panic: %v path=%s", rec, r.URL.Path)
http.Error(w, http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func hello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello", hello)
http.ListenAndServe(":8080", recoverMiddleware(mux))
}Cuándo usar esto:
defer para cerrar cuerpos de solicitud y finalizar spans de observabilidad por solicitud.next.package main
import (
"context"
"log"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func timeoutMiddleware(d time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), d)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func main() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Recoverer) // middleware defer/recover de chi
r.Use(timeoutMiddleware(2 * time.Second))
r.Get("/boom", func(w http.ResponseWriter, r *http.Request) {
panic("developer mistake")
})
r.Get("/ok", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("fine"))
})
log.Println(http.ListenAndServe(":8080", r))
}Lo que esto demuestra:
Recoverer de Chi aplica el patrón estándar de defer/recover en todas las rutas.timeoutMiddleware difiere cancel() para liberar recursos del temporizador cuando la solicitud finaliza.r.WithContext sin variables globales./boom se convierte en 500; /ok todavía se sirve normalmente.net/http; un panic solo desenrolla esa goroutine.http.Handler; el control pasa linealmente a menos que el middleware retorne anticipadamente.defer recover debe estar en la misma función que invoca al manejador (o llama a next.ServeHTTP).Recovery() con pilas de defer similares y trazas de pila opcionales.recover son inofensivas pero redundantes; elige una recuperación externa.Solicitud ─► Logging ─► Recover ─► Auth ─► Handler
│ │ │
│ │ └─ retornar 401 (omitir manejador)
│ └─ defer recover en panic
└─ defer registrar latencia
| Framework | Ayuda de Recuperación | Notas |
|---|---|---|
| stdlib | defer recover escrito a mano | Control total, sin dependencia |
| chi | middleware.Recoverer | Se integra en la cadena r.Use |
| gin | gin.Recovery() | El motor predeterminado incluye logger + recuperación |
| echo | middleware.Recover() | Registro de traza de pila configurable |
// Cortocircuito de autenticación: no llamar a next cuando no está autorizado
func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}debug.Stack().Use.defer body.Close() - r.Body filtra conexiones en errores. Solución: defer r.Body.Close() en manejadores o middleware compartido después del enrutamiento.http.Error con el estado adecuado para fallos esperados.r.Context() en llamadas de I/O o los tiempos de espera nunca se activarán. Solución: pasar el contexto a http.NewRequestWithContext, llamadas a bases de datos y stubs gRPC.| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
Middleware Recovery() del framework | Arranque rápido con gin/echo/chi | Necesitas una forma personalizada de métricas de panic |
http.Server panic en manejador | Nunca para producción | - |
| Supervisor de proceso solamente | Trabajadores por lotes | Servidores HTTP de larga duración |
| Retornos de error en lugar de panic | Validación de negocio | - |
| Interceptores gRPC para RPC unarias | Servicios gRPC | Manejadores HTTP simples |
Fuera del manejador, típicamente al principio de la cadena.
La ubicación externa captura los panics de los middleware internos también.
No, convierte un panic en desenrollado en una respuesta 500.
El cliente debe reintentar; la goroutine del servidor continúa sirviendo otras solicitudes.
Sí, cerrar cuerpos, vaciar búferes, finalizar spans.
Mantén los defer ligeros; evita trabajo pesado en defer en rutas activas.
Gin registra la pila y retorna 500 de manera similar al middleware personalizado.
Ajusta con gin.RecoveryWithWriter para el enrutamiento de logs.
El middleware de Echo puede deshabilitar o personalizar la salida de la pila.
Apúntalo a logs estructurados en producción.
Raramente; el middleware es lineal, no bucles anidados.
Usa return para cortocircuitar.
Lee de middleware.GetReqID de chi o configúralo en middleware personalizado en el contexto.
Incluye el ID en los logs de panic.
Sí, los interceptores unarios/de flujo reflejan la recuperación HTTP.
Usa los patrones de recuperación de google.golang.org/grpc para servidores RPC.
Solo si los manejadores respetan r.Context().Done().
Las llamadas bloqueantes sin contexto ignoran los tiempos de espera.
Los panics al inicio fallan rápido antes de aceptar tráfico.
Las rutas de solicitud en tiempo de ejecución no deben hacer panic por la entrada del usuario.
Versiones de Stack: Esta página fue escrita para Go 1.26.x (predeterminado GC Green Tea,
go fix modernizers- verificar parche en la compilación), chi (última versión - verificar en la compilación), gin (última versión - verificar en la compilación), echo (última versión - verificar en la compilación), google.golang.org/grpc (última versión - verificar en la compilación), sigs.k8s.io/controller-runtime (última versión - verificar en la compilación), kubebuilder (última versión - verificar en la compilación), tinygo (última versión - verificar objetivos de placa en la compilación), wazero (última versión - verificar en la compilación), y golangci-lint (última versión - verificar conjunto de linters en la compilación).
Revisado por Chris St. John·Última actualización: 18 jul 2026