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 siDEBUG=True):str(exc)— el mensaje técnico de la excepción. - Campo
type(solo siDEBUG=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: ConDEBUG=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: ConDEBUG=True, verifica que:- El campo
errorsigue siendo"Internal server error". - El campo
detailcontiene el str(exc) original. - El campo
typecontiene el nombre de la clase.
- El campo
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 asettings.DEBUG.logging— Para el loggerapi.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]]