Manejo de errores y respuestas HTTP personalizadas

Video
20 min~11 min lectura
Objetivo de la lección

Un manejo de errores deficiente puede convertir un problema manejable en una pesadilla de depuración, ocultando la causa raíz y frustrando a los consumidores del servicio.

Puntos de control
  • Introducción al Manejo de Errores en APIs REST con Go
  • Concepto Clave: La Filosofía de las Respuestas HTTP Estructuradas
  • Cómo Funciona en la Práctica: De Error a Respuesta
  • Código en Acción: Implementación de un Sistema de Errores

Reproductor de video

Introducción al Manejo de Errores en APIs REST con Go

En el desarrollo de microservicios de alto rendimiento, la comunicación clara y consistente entre el cliente y el servidor es fundamental. Mientras que las respuestas exitosas (códigos 2xx) suelen recibir toda la atención, es en el manejo de situaciones inesperadas o erróneas donde la robustez y profesionalismo de una API realmente se ponen a prueba. Un manejo de errores deficiente puede convertir un problema manejable en una pesadilla de depuración, ocultando la causa raíz y frustrando a los consumidores del servicio.

Esta lección se centra en transformar el manejo de errores de una ocurrencia tardía a un componente de diseño central de tu API. Utilizando Gorilla/Mux como nuestro enrutador, exploraremos cómo estructurar respuestas de error informativas, consistentes y que sigan las mejores prácticas de HTTP. Abordaremos desde la definición de un formato de error común hasta la implementación de middleware especializado y el manejo elegante de pánicos, asegurando que tu servicio no solo sea rápido, sino también confiable y fácil de integrar.

El objetivo final es pasar de devolver mensajes de error genéricos y potencialmente inseguros, a proporcionar respuestas estructuradas que guíen al cliente desarrollador hacia la resolución del problema, manteniendo al mismo tiempo la seguridad y el rendimiento del sistema.

Concepto Clave: La Filosofía de las Respuestas HTTP Estructuradas

Imagina que llamas a un servicio de atención al cliente técnico. Una mala experiencia sería escuchar un mensaje grabado que solo dice: "Algo salió mal. Adiós." y luego colgar. Una experiencia excelente implicaría que un agente te informe: "Lo siento, el sistema de facturación está experimentando latencia (Error #DB-4422). Hemos registrado el incidente. Por favor, reintenta tu solicitud en 2 minutos o, si el problema persiste, contacta a soporte con el ID de referencia REF-ABC123." El manejo de errores en una API debe aspirar a ser este último agente: útil, informativo y que permita una acción correctiva.

En el contexto HTTP/REST, esto se traduce en dos principios: usar los códigos de estado HTTP correctamente y proporcionar un cuerpo de respuesta estructurado. El código de estado (404, 400, 500, etc.) le da al cliente (navegador, otra aplicación, SDK) una comprensión inmediata y automatizable del tipo de error. El cuerpo, típicamente en JSON, proporciona los detalles humanos y técnicos necesarios para entender y resolver el problema. Separar estos dos canales de información es crucial para construir clientes resilientes.

Un error común es malinterpretar los códigos. Un 400 Bad Request indica que el servidor no puede o no quiere procesar la solicitud debido a algo que se percibe como un error del *cliente* (validación, JSON malformado). Un 500 Internal Server Error indica un fallo inesperado en el *servidor* para el cual el cliente no tiene culpa. Mezclar estos códigos confunde a los consumidores de la API sobre quién es responsable de la corrección. Nuestro sistema debe mapear de manera confiable los errores de la lógica de negocio y de la infraestructura a los códigos de estado apropiados.

Cómo Funciona en la Práctica: De Error a Respuesta

El flujo práctico comienza con la definición de una estructura de datos canónica para todos los errores de nuestra API. Esta estructura será la que serializaremos a JSON en el cuerpo de la respuesta cuando algo falle. Un diseño típico y efectivo incluye campos como: un mensaje legible para humanos, un código de error interno para el equipo de desarrollo, un detalle técnico opcional, y una traza o ID de correlación para rastrear el error en los logs. Esta estructura se crea en el punto donde se detecta el error dentro de un handler o middleware.

Una vez creado el objeto de error, el siguiente paso es determinar el código de estado HTTP apropiado. Aquí es donde la lógica de negocio se encuentra con el protocolo. Un error de "usuario no encontrado" podría ser un 404. Un error de "contraseña incorrecta" sería un 401 Unauthorized. Un error de validación de un campo requerido es un 400. Para errores inesperados (como un fallo al conectar con la base de datos), se debe devolver un 500. Gorilla/Mux no impone esta lógica; nosotros la implementamos usando funciones auxiliares que toman el error, lo envuelven en nuestra estructura, deciden el código HTTP y escriben la respuesta.

Finalmente, para evitar la repetición de código y asegurar la consistencia, este proceso se encapsula a menudo en un middleware global de manejo de errores o en funciones helper dedicadas. El middleware puede capturar pánicos (usando `recover`) y convertirlos en respuestas 500 estructuradas, evitando que el proceso del servidor se detenga y proporcionando una experiencia controlada. Los handlers individuales entonces se enfocan en la lógica feliz, delegando la gestión de errores a estas utilidades centralizadas.

Código en Acción: Implementación de un Sistema de Errores

Comencemos definiendo nuestra estructura de error y las funciones helper. Este código establece el contrato para todas las respuestas de error en nuestra API.


package api

import (
    "encoding/json"
    "log"
    "net/http"
    "runtime/debug"
)

// AppError representa la estructura canónica de un error en la API.
type AppError struct {
    StatusCode int    `json:"status"`           // Código HTTP (ej: 400, 404, 500)
    Message    string `json:"message"`          // Mensaje legible para el usuario/desarrollador cliente
    Code       string `json:"code,omitempty"`   // Código de error interno de la aplicación (ej: "USER_NOT_FOUND")
    Detail     string `json:"detail,omitempty"` // Detalle técnico (no en producción)
    TraceID    string `json:"trace_id,omitempty"` // ID para correlacionar con logs
}

// Error implementa la interfaz error estándar.
func (e *AppError) Error() string {
    return e.Message
}

// NewAppError crea un nuevo AppError.
func NewAppError(statusCode int, message, internalCode string) *AppError {
    return &AppError{
        StatusCode: statusCode,
        Message:    message,
        Code:       internalCode,
    }
}

// WriteError escribe una respuesta HTTP en formato JSON a partir de un AppError.
func WriteError(w http.ResponseWriter, err *AppError) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(err.StatusCode)
    json.NewEncoder(w).Encode(err)
}

// ErrorHandler es un middleware que convierte pánicos en respuestas 500 estructuradas.
func ErrorHandler(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 recuperado: %v\n%s", rec, debug.Stack())
                err := NewAppError(
                    http.StatusInternalServerError,
                    "Ocurrió un error interno inesperado en el servidor.",
                    "INTERNAL_PANIC",
                )
                err.Detail = "Se recuperó un pánico en el handler."
                // En un entorno real, generaríamos un TraceID único aquí.
                WriteError(w, err)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

Ahora, veamos cómo se utiliza este sistema en un handler concreto con Gorilla/Mux. El handler se vuelve más limpio y enfocado en la lógica de negocio.


package main

import (
    "database/sql"
    "encoding/json"
    "net/http"
    "yourproject/api" // Paquete donde definimos AppError y WriteError
    "github.com/gorilla/mux"
    "github.com/google/uuid"
)

type UserHandler struct {
    DB *sql.DB
}

// GetUserByID maneja GET /users/{id}
func (h *UserHandler) GetUserByID(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    userIDStr := vars["id"]

    // 1. Validación de entrada
    userID, err := uuid.Parse(userIDStr)
    if err != nil {
        // Usamos nuestro helper para error de cliente
        apiErr := api.NewAppError(
            http.StatusBadRequest,
            "El ID de usuario proporcionado no tiene un formato válido.",
            "INVALID_UUID",
        )
        api.WriteError(w, apiErr)
        return
    }

    // 2. Lógica de negocio (ejemplo con consulta a BD)
    var userName string
    query := `SELECT name FROM users WHERE id = $1`
    err = h.DB.QueryRowContext(r.Context(), query, userID).Scan(&userName)

    // 3. Manejo de errores específicos de la lógica
    if err != nil {
        if err == sql.ErrNoRows {
            // Recurso no encontrado -> 404
            apiErr := api.NewAppError(
                http.StatusNotFound,
                "No se encontró un usuario con el ID especificado.",
                "USER_NOT_FOUND",
            )
            api.WriteError(w, apiErr)
        } else {
            // Error inesperado de base de datos -> 500
            // Log del error real internamente (¡no lo enviamos al cliente!)
            log.Printf("Error de BD al buscar usuario %v: %v", userID, err)
            apiErr := api.NewAppError(
                http.StatusInternalServerError,
                "No se pudo recuperar la información del usuario.",
                "DB_QUERY_FAILED",
            )
            // Podríamos añadir un TraceID generado en el middleware de logging.
            api.WriteError(w, apiErr)
        }
        return
    }

    // 4. Respuesta exitosa
    user := map[string]string{"id": userID.String(), "name": userName}
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(user)
}

func main() {
    r := mux.NewRouter()
    // ¡Aplicamos el middleware de manejo de pánicos a TODA la ruta!
    r.Use(api.ErrorHandler)

    userHandler := &UserHandler{DB: obtenerConexionBD()} // Función hipotética
    r.HandleFunc("/users/{id}", userHandler.GetUserByID).Methods("GET")

    http.ListenAndServe(":8080", r)
}

Para completar el panorama, implementemos un middleware de logging que registre todas las peticiones y respuestas, incluyendo los errores, y añada un ID de correlación. Este ID puede luego ser usado en la respuesta de error para facilitar el rastreo.


package api

import (
    "context"
    "log"
    "net/http"
    "time"
    "github.com/google/uuid"
)

// key tipo para evitar colisiones en context.Context
type contextKey string

const TraceIDKey contextKey = "trace_id"

// LoggingMiddleware registra información de la solicitud y respuesta, e inyecta un TraceID.
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        traceID := uuid.New().String()

        // Inyectar el TraceID en el contexto de la petición
        ctx := context.WithValue(r.Context(), TraceIDKey, traceID)
        r = r.WithContext(ctx)

        // Usamos un ResponseWriter personalizado para capturar el código de estado
        rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        
        // Procesar la petición
        next.ServeHTTP(rw, r)

        // Log después de procesar
        duration := time.Since(start)
        log.Printf(
            "[%s] %s %s - %d - %v",
            traceID,
            r.Method,
            r.RequestURI,
            rw.statusCode,
            duration,
        )
    })
}

// responseWriter personalizado para capturar el código de estado.
type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// Helper para obtener el TraceID del contexto y usarlo en WriteError.
func WriteErrorWithTrace(w http.ResponseWriter, r *http.Request, err *AppError) {
    if traceID, ok := r.Context().Value(TraceIDKey).(string); ok {
        err.TraceID = traceID
    }
    WriteError(w, err)
}
// En el handler, ahora usaríamos api.WriteErrorWithTrace(w, r, apiErr)

Errores Comunes y Cómo Evitarlos

1. Filtrar detalles internos de errores al cliente: El error más grave es exponer información sensible como stack traces, sentencias SQL o rutas de archivo internas en respuestas de producción. Esto es una brecha de seguridad.

Siempre loguea el error completo con todos sus detalles internamente (en tus servidores/logs), pero envíale al cliente solo un mensaje genérico y un código de error o ID de correlación. Usa estructuras como AppError para separar claramente el Message (para el cliente) del Detail (solo para desarrollo).

2. Usar códigos de estado HTTP incorrectos: Devolver 200 OK con un cuerpo que dice {"error": "not found"} o usar 500 para errores de validación del cliente. Esto rompe el contrato HTTP y confunde a los clientes automatizados.

Respeta la semántica HTTP. Usa 4xx para errores del cliente (400, 401, 403, 404, 422) y 5xx solo para fallos inesperados del servidor. Investiga códigos más específicos como 409 Conflict o 429 Too Many Requests cuando sean apropiados.

3. No tener un formato de error consistente: Que un endpoint devuelva errores en JSON, otro en texto plano y otro sin cuerpo. Esto obliga a los consumidores de la API a escribir lógica ad-hoc para cada endpoint.

Establece un estándar de equipo/proyecto para la estructura de error (como AppError) y utilízalo en TODOS los endpoints. Un middleware de renderizado final puede ayudar a garantizar esta consistencia.

4. Olvidar manejar los pánicos (panic): Si una goroutine entra en pánico dentro de un handler y no se recupera, el proceso completo del servidor puede terminar, causando downtime.

Envuelve tu router principal o todos los handlers con un middleware de recuperación de pánicos (como nuestro ErrorHandler) que capture el recover(), lo loguee y devuelva una respuesta 500 controlada.

5. No proveer un ID de correlación (TraceID): Cuando un cliente reporta un error, es extremadamente difícil encontrarlo en los logs del servidor si no hay una clave única que vincule la petición HTTP con las entradas de log.

Genera un UUID único al inicio de cada petición (en un middleware de logging), guárdalo en el contexto de la petición y asegúrate de que aparezca en todos los logs relacionados con esa petición y en la respuesta de error enviada al cliente.

Checklist de Dominio

  • ¿He definido una estructura de datos canónica (ej: AppError) para todas las respuestas de error de mi API?
  • ¿Estoy utilizando correctamente los códigos de estado HTTP (4xx para errores del cliente, 5xx para errores del servidor) en cada situación de error?
  • ¿He implementado un middleware de recuperación de pánicos (recover) que prevenga la caída del servidor y devuelva una respuesta 500 estructurada?
  • ¿Mi sistema loguea todos los detalles técnicos de los errores internamente, pero solo envía mensajes seguros y genéricos al cliente (evitando filtrar stack traces, consultas SQL, etc.)?
  • ¿Cada petición tiene un ID de correlación único (TraceID) que aparece tanto en los logs del servidor como en la respuesta de error enviada al cliente?
  • ¿Todos los endpoints de mi API devuelven errores en un formato JSON consistente, independientemente de quién lance el error (handler, middleware, función helper)?
  • ¿He probado manualmente o con tests automatizados los flujos de error comunes (datos inválidos, recursos no encontrados, falta de autorización, errores internos simulados)?
  • ¿La documentación de mi API (OpenAPI/Swagger) describe claramente los posibles códigos de error y el formato de las respuestas de error para cada operación?
Falar no WhatsApp
De lección a portfolio

Convertí esta lección en una prueba técnica visible.

Una app pequeña publicada, con README y decisiones explicadas, funciona mejor que una lista de tecnologías sueltas.

Paso 1

Creá una demo mínima que use el concepto de la lección.

Paso 2

Escribí un README corto con objetivo, stack, decisión técnica y mejora futura.

Paso 3

Publicá la demo y enlazala desde tu perfil profesional.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión

Manejo de errores y respuestas HTTP personalizadas | Cursalo