CreaRack-SL

global_exception_handler · Manejador global de excepciones API

Descripción

Handler global de excepciones para todos los ~509 endpoints de la API Ninja de CreaRack. Se encarga de capturar cualquier excepción no manejada en los endpoints y traducirla a una respuesta HTTP 500 estandarizada.

Ubicación: config/urls.py

Función pública: global_exception_handler(request, exc) → Response

Interfaz

@api.exception_handler(Exception)
def global_exception_handler(request, exc):
    """
    Handler registrado globalmente en la API Ninja.
    
    Args:
        request: HttpRequest del endpoint que falló.
        exc: Exception capturada.
    
    Returns:
        Response HTTP 500 con cuerpo estandarizado.
    """

Comportamiento

Producción (DEBUG=False)

Respuesta al cliente:

{
  "error": "Internal server error"
}

Logs (registro en api.errors):

ERROR:api.errors:Unhandled API error on GET /api/racks/list
  <full traceback con exc_info=exc>

Motivación: Evitar fuga de información sensible (rutas internas, SQL, hosts).

Desarrollo local (DEBUG=True)

Respuesta al cliente:

{
  "error": "Internal server error",
  "detail": "<str(exc)>",
  "type": "ExceptionClassName"
}

Logs: Igual que en producción.

Motivación: Facilitar debugging local sin perder contexto.

Componentes internos

Logger (_api_logger)

  • Nombre: "api.errors"
  • Función: Registrar el traceback completo con contexto HTTP (método, path).
  • Nivel: ERROR
  • Uso: _api_logger.error("Unhandled API error on %s %s", request.method, request.path, exc_info=exc)

Cuerpo de respuesta

  • Campo error: Siempre "Internal server error" (fijo y genérico).
  • Campo detail (solo si DEBUG=True): str(exc) — el mensaje técnico de la excepción.
  • Campo type (solo si DEBUG=True): Nombre de la clase de la excepción.

Contexto histórico

Antes (hasta commit 4e82336): El handler devolvía {"error": str(exc), "type": exc.__class__.__name__} incondicionalmente, exponiendo información sensible (rutas del servidor, consultas SQL, nombres de máquinas) a cualquier cliente que provocara un error.

Hallazgo s105: Auditoría de seguridad (Auditoría Suprema Etapa 3 · T4) identificó esto como fuga de información transversal a todos los endpoints.

Cambio (commit 4e82336): Implementación del nuevo handler con cierre de fuga (decision T4).

Extensión a manejadores locales (D1-04, mega-auditoría 24-09-2026, v1.157.0)

El handler global solo actúa cuando ningún except local atrapa antes la excepción. Catorce endpoints seguían con su propio except Exception as e: return N, {"message": str(e)} — el handler global nunca llegaba a verlos, así que el mismo texto crudo (rutas, SQL, mensajes de librerías) se colaba igual, endpoint a endpoint:

  • racks/api/library.py — create_custom_stencil (×3 variantes), delete_library_category, rename_library_category, cleanup_session.
  • racks/api/library_files.py — importar Visio, analizar y confirmar Visio, copia, analizar y confirmar restauración.
  • core/api/users.py — _create_user_impl.
  • network/api/scripts.py — execute_script.

Mismo patrón que el handler global: logger.exception("<contexto>") deja el detalle en el log, la respuesta HTTP pasa a un mensaje fijo con el mismo código. Los errores de dominio pensados para que el usuario los lea se conservan aparte (capturados por su tipo, no por Exception genérico): SVG no válido al crear un stencil, o ZIP con ruta insegura/demasiado grande al analizar una restauración (este último además pasa de 500 a 400, que es el código que le corresponde). En racks/services/library.py, el servicio de importación de Visio envolvía cualquier fallo en ValueError(f"Parsing failed: {e}") que el endpoint devolvía tal cual — ahora registra el detalle y relanza ValueError("Parsing failed") fijo; como contrapartida, los avisos propios del parser (formato no soportado, sin figuras) también se ven solo como “Parsing failed”.

Límite honesto: terminal/api/sentinel_ingest.py (ingesta del Agente local) se queda igual, con "error": str(e) en su 500 — un test existente (tests/api/test_bulk_ingest_honest_status.py) exige ahí el texto crudo. Cambiarlo es una decisión de producto aparte, no un olvido.

Pruebas

Archivo: tests/api/test_t4_exception_handler.py

  • test_500_body_is_generic: Con DEBUG=False, verifica que:

    • Status code es 500.
    • Cuerpo es exactamente {"error": "Internal server error"}.
    • No hay información sensible en la respuesta (ej. rutas, nombres de excepciones).
  • test_debug_keeps_detail_for_local_dev: Con DEBUG=True, verifica que:

    • El campo error sigue siendo "Internal server error".
    • El campo detail contiene el str(exc) original.
    • El campo type contiene el nombre de la clase.

Impacto en endpoints

Alcance: Todos los ~509 endpoints de la API Ninja (handler global) + 14 endpoints con manejo local propio (D1-04, v1.157.0).

Cambio observable en el cliente:

  • Antes: Respuesta 500 con detalles técnicos variables según la excepción.
  • Después: Respuesta 500 con cuerpo genérico. El frontend debe asumir “error genérico” sin detalles al usuario final.

Verificación: Nadie en el codebase consumía el campo type del 500 → frontend sin cambios necesarios.

Configuración

Dependencias:

  • django.conf.settings — Para acceder a settings.DEBUG.
  • logging — Para el logger api.errors.
  • ninja.errors — La API Ninja y sus funciones de respuesta.

Registración: Se registra globalmente en la instancia api de Ninja a través del decorador @api.exception_handler(Exception).

Véase también

  • [[decision—20260611—t4-handler-excepciones-sin-fuga-informacion]]
  • [[concept—security—information-disclosure]]
  • [[runbook—security—respond-error-disclosure]]