Cazador de Fallos Silenciosos

Auditoría de Fallos Silenciosos

NutriTrack SaaS — Backend API · Node.js 18 + TypeScript

📁 4 archivos auditados
📅 18 Jun 2026
🔬 Agente: Silent Failure Hunter v1.0
⚙️ Stack: Express · pg · Redis · Stripe
3
Críticos
2
Altos
2
Medios
7
Total hallazgos
Hallazgos detallados
1
catch vacío traga todos los errores de envío de email y Redis
reminderService.ts · líneas 9-12 (bucle for)
Crítico
Empty Catch Sin logging
Problema
catch {} sin cuerpo: cualquier fallo de sendEmail o de Redis queda completamente silenciado. El sistema cree que los recordatorios se enviaron cuando en realidad fallaron.
Impacto en producción
Los pacientes no reciben sus recordatorios de ingesta. La clínica pierde adherencia del paciente y no hay ninguna señal de alerta para el equipo de operaciones. Diagnóstico imposible retroactivamente.
// ❌ ANTES — fallo completamente silencioso try { await sendEmail(patient.email, 'Recuerda registrar...'); await redis.set(...); } catch {} ← TRAGA TODOS LOS ERRORES // ✅ DESPUÉS — logging + conteo + alerta cuando umbral supera } catch (err) { failedCount++; logger.error({ err, patientId: patient.id, email: patient.email }, 'reminder.send.failed'); } // Al final del bucle: if (failedCount > 0) { metrics.increment('reminders.failed', failedCount); if (failedCount / patients.rows.length > 0.1) alerting.fire('reminder_high_failure'); }
Recomendación
Separar el try del email del try de Redis. Loguear cada fallo con patientId y email. Añadir métrica de tasa de fallos y alertar si supera el 10% en un lote.
2
Fallback a null oculta fallos de cobro Stripe — pérdida económica silenciosa
paymentService.ts · líneas 15-19 (catch chargeResult = null)
Crítico
Fallback peligroso Logging ausente Sin propagación
Problema
Cuando stripe.charges.create lanza una excepción (tarjeta rechazada, timeout, límite de rate), el error queda convertido en null. No hay log, no se notifica al admin, no se registra el intento fallido en la BD.
Impacto en producción
La clínica mantiene acceso activo a la plataforma sin pagar. La empresa pierde ingresos sin saberlo. Los intentos fallidos no se reintentan porque no hay registro de que ocurrieron. Riesgo de auditoría financiera.
// ❌ ANTES — chargeResult = null oculta el fallo de Stripe } catch (err) { chargeResult = null; ← fallo financiero silencioso } // ✅ DESPUÉS — registrar intento, re-lanzar para retry, alertar } catch (err) { await db.query( 'INSERT INTO payment_failures(clinic_id, error, occurred_at) VALUES($1,$2,NOW())', [clinicId, (err as Error).message] ); logger.error({ err, clinicId }, 'payment.renewal.failed'); alerting.fire('payment_failure', { clinicId }); throw err; ← permite sistema de retry externo }
Recomendación
Registrar el fallo en tabla payment_failures, loguear con contexto completo, notificar al equipo financiero por Slack/email y re-lanzar la excepción para que el job scheduler pueda hacer retry con backoff exponencial.
3
JSON.parse sin validación + catch silencioso devuelve datos corruptos
patientRoutes.ts · líneas 12-15 (GET /nutrition-log cache)
Crítico
Empty Catch Fallback engañoso
Problema
JSON.parse(null) lanza excepción porque redis.get devuelve null en cache miss. El catch silencia esto y deja cachedData = []. Si Redis está caído, nunca hay error visible.
Impacto en producción
Cuando Redis falla, el endpoint parece funcionar correctamente (cae al fallback de BD), pero enmascara una degradación de infraestructura. También: si el JSON en Redis está corrupto, se sirven silenciosamente datos vacíos al paciente.
// ❌ ANTES } catch (e) { // cache miss, no problem ← ERROR: también traga fallos reales de Redis } // ✅ DESPUÉS — distinguir cache miss de error de Redis const raw = await redis.get(`nutrition:${id}`); if (raw !== null) { try { cachedData = JSON.parse(raw); } catch (parseErr) { logger.warn({ parseErr, key: `nutrition:${id}` }, 'cache.json.corrupt'); redis.del(`nutrition:${id}`); ← limpiar entrada corrupta } }
Recomendación
Distinguir explícitamente cache miss (raw === null) de error de parsing. Loguear JSON corrupto en Redis como warning e invalidar la clave. Si Redis lanza error de conexión, registrar como error de infraestructura.
4
redis.set sin await y sin manejo de error en GET /nutrition-log
patientRoutes.ts · línea 28 (fire-and-forget)
Alto
Async incorrecto Fire-and-forget
Problema
redis.set(...) sin await ni .catch(). Una promesa rechazada sin manejar provoca UnhandledPromiseRejection en Node.js, que en versiones recientes termina el proceso.
Impacto en producción
Si Redis está degradado, cada petición GET puede matar el proceso de Node.js, causando indisponibilidad completa de la API. Además, la caché nunca se rellena, así que cada petición va a BD.
// ❌ ANTES — fire-and-forget peligroso redis.set(`nutrition:${id}`, JSON.stringify(result.rows)); // ✅ DESPUÉS — con TTL, await opcional con .catch() defensivo redis.set(`nutrition:${id}`, JSON.stringify(result.rows), 'EX', 300) .catch(err => logger.warn({ err, patientId: id }, 'cache.set.failed'));
Recomendación
Añadir .catch() para absorber el error sin romper el flujo. Añadir TTL explícito (recomendado: 300 s) para evitar datos obsoletos indefinidamente.
5
Log sin stack trace ni contexto en POST /meal
patientRoutes.ts · línea 42 (console.log en catch)
Alto
Logging insuficiente
Problema
console.log('Error saving meal') sin incluir el objeto err, el patientId ni el payload. Se pierde el stack trace y cualquier contexto de diagnóstico.
Impacto en producción
Cuando falla la inserción de un registro de ingesta, el log solo dice "Error saving meal". No es posible saber qué paciente, qué alimento, qué tipo de error ni qué query falló. El tiempo de diagnóstico se multiplica por 10.
// ❌ ANTES console.log('Error saving meal'); ← inútil para debugging // ✅ DESPUÉS logger.error({ err, patientId: id, payload: { calories, food_name, meal_type } }, 'meal.insert.failed');
Recomendación
Usar un logger estructurado (pino/winston) con nivel error, incluyendo el error completo, el patientId y los campos del payload (omitiendo datos sensibles si aplica).
6
Pool de PostgreSQL sin timeouts — conexiones colgadas indefinidamente
lib/db.ts · líneas 4-7 (configuración Pool)
Medio
Sin timeout Sin event handler
Problema
El Pool de pg no tiene connectionTimeoutMillis, idleTimeoutMillis ni statement_timeout. El evento pool.on('error') no está registrado.
Impacto en producción
Una query lenta o un spike de tráfico puede agotar el pool de 10 conexiones sin ninguna señal. Las peticiones se cuelgan indefinidamente hasta timeout de cliente. El evento de error no capturado en el pool puede terminar el proceso.
// ❌ ANTES — sin timeouts ni handler de errores export const db = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 }); // ✅ DESPUÉS export const db = new Pool({ connectionString: process.env.DATABASE_URL, max: 10, connectionTimeoutMillis: 3_000, idleTimeoutMillis: 30_000, statement_timeout: 10_000, }); db.on('error', (err) => logger.error({ err }, 'pg.pool.error'));
Recomendación
Configurar connectionTimeoutMillis: 3000, idleTimeoutMillis: 30000 y statement_timeout: 10000. Registrar pool.on('error') para capturar errores de cliente inactivo sin romper el proceso.
7
redis.del sin await ni manejo de error en POST /meal
patientRoutes.ts · línea 36 (.catch(() => {}))
Medio
Async incorrecto Logging ausente
Problema
.catch(() => {}) traga el error de invalidación de caché sin registrarlo. Si Redis está caído, los pacientes ven datos obsoletos pero no hay ninguna señal.
Impacto en producción
Tras registrar una comida, el endpoint GET sigue sirviendo la caché desactualizada. El paciente ve datos incorrectos de su diario nutricional. Al no haber log, el equipo no sabe que Redis está fallando.
// ❌ ANTES redis.del(`nutrition:${id}`).catch(() => {}); ← fallo invisible // ✅ DESPUÉS redis.del(`nutrition:${id}`) .catch(err => logger.warn({ err, patientId: id }, 'cache.invalidate.failed'));
Recomendación
Registrar el error de invalidación como warn con contexto del patientId. Si el patrón de fallos se repite, activar alerta de salud de Redis.
Matriz de hallazgos por archivo
# Archivo Tipo de fallo Severidad Patrón detectado
F-001 reminderService.ts:9 Empty Catch Crítico catch {} — traga errores email + Redis
F-002 paymentService.ts:15 Fallback peligroso Crítico chargeResult = null — oculta fallo Stripe
F-003 patientRoutes.ts:12 Empty Catch Crítico JSON.parse(null) con catch mudo
F-004 patientRoutes.ts:28 Async incorrecto Alto Fire-and-forget sin .catch() — posible crash
F-005 patientRoutes.ts:42 Logging insuf. Alto console.log sin err ni contexto
F-006 lib/db.ts:4 Sin timeout Medio Pool sin timeouts ni on('error')
F-007 patientRoutes.ts:36 Async incorrecto Medio .catch(() => {}) sin log
Plan de acción prioritario