Webhooks explicados: cargas firmadas y estrategias de reintento
Acabas de integrar una pasarela de pago o un servicio de CI/CD, y ahora necesitas recibir eventos en tiempo real. Configuras un endpoint, pero pronto te enfrentas a dos preguntas críticas: ¿Cómo aseguras que la solicitud realmente provino del servicio? ¿Y qué sucede si tu servidor está caído cuando se dispara un evento? Este artículo responde a ambas explicando las cargas firmadas y las estrategias de reintento, la base de los webhooks confiables.
¿Qué son los webhooks?
Los webhooks son callbacks HTTP definidos por el usuario. Cuando ocurre un evento en un sistema de origen (por ejemplo, un nuevo commit, un pago exitoso), envía una solicitud HTTP POST a una URL que proporcionas. A diferencia del sondeo, los webhooks envían datos casi en tiempo real, reduciendo la latencia y la carga del servidor. Sin embargo, este modelo de envío introduce desafíos: verificar la autenticidad y manejar fallos de entrega.
Por qué importan las cargas firmadas
Cualquiera puede enviar una solicitud HTTP a tu endpoint. Sin verificación, los atacantes podrían falsificar eventos, lo que llevaría a acciones no autorizadas. Las cargas firmadas resuelven esto al incluir una firma criptográfica calculada sobre el cuerpo de la solicitud usando un secreto compartido. El receptor recalcula la firma y la compara, asegurando que la carga no ha sido manipulada y proviene del remitente esperado.
Cómo funcionan las firmas HMAC
La mayoría de los servicios usan HMAC-SHA256. El remitente crea una firma hasheando la carga con una clave secreta y la incluye en un encabezado (por ejemplo, X-Signature). El receptor hace lo mismo y compara usando una función de tiempo constante para prevenir ataques de temporización.
Paso a paso: Verificar una carga firmada
- Extrae la firma del encabezado (por ejemplo,
X-Signature). - Obtén el cuerpo raw de la solicitud como string o buffer—no lo parsees primero, ya que los cambios de formato rompen la firma.
- Calcula la firma esperada usando HMAC-SHA256 con tu clave secreta.
- Compara las firmas usando una función de comparación de tiempo constante.
- Rechaza la solicitud con HTTP 401 si no coinciden.
Aquí tienes un ejemplo en Node.js usando Express:
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const payload = req.body; // raw buffer
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// Process the event
const event = JSON.parse(payload);
// ... handle event
res.status(200).send('OK');
});
Nota: Usa siempre el cuerpo raw para la verificación. El middleware que parsea JSON alterará la carga, causando discrepancias en la firma.
Diseñar una estrategia de reintento
Incluso con cargas firmadas, los problemas de red o el tiempo de inactividad del servidor pueden causar fallos de entrega. Una estrategia de reintento robusta asegura que los eventos no se pierdan. Consideraciones clave:
1. Acuse de recibo rápido
Responde con un estado 2xx tan pronto como hayas validado la firma y encolado el evento para su procesamiento. Esto evita que el remitente agote el tiempo de espera y reintente innecesariamente.
2. Idempotencia
Los reintentos pueden entregar el mismo evento varias veces. Usa una clave de idempotencia (a menudo incluida en la carga o en los encabezados) para deduplicar. Almacena los IDs de eventos procesados e ignora los duplicados.
3. Retroceso exponencial con jitter
Si tu endpoint devuelve un error (no 2xx), el remitente debe reintentar con retrasos crecientes. El retroceso exponencial (por ejemplo, 1s, 2s, 4s, 8s) reduce la carga durante las interrupciones. Añadir jitter aleatorio previene problemas de estampida.
Programa de reintentos típico:
| Intento | Retraso |
|---|---|
| 1 | Inmediato |
| 2 | 1 minuto |
| 3 | 5 minutos |
| 4 | 30 minutos |
| 5 | 2 horas |
4. Cola de mensajes muertos
Después de los reintentos máximos, mueve el evento a una cola de mensajes muertos para inspección manual. Esto previene reintentos infinitos y pérdida de datos.
5. Monitoreo y alertas
Rastrea las tasas de éxito de entrega, la latencia y las razones de fallo. Configura alertas para picos de fallos para detectar problemas temprano.
Errores comunes y mejores prácticas
- Verifica siempre las firmas—nunca confíes ciegamente en los datos entrantes.
- Usa HTTPS para proteger las cargas en tránsito.
- Rota los secretos periódicamente y admite múltiples secretos activos durante la rotación.
- Registra los eventos de webhook (con datos sensibles redactados) para depuración.
- Prueba con cargas de ejemplo usando herramientas que simulan entregas de webhooks.
Preguntas frecuentes
¿Qué es una carga firmada en webhooks?
Una carga firmada incluye una firma criptográfica (generalmente HMAC) que permite al receptor verificar la autenticidad e integridad de la solicitud.
¿Cuántas veces se debe reintentar un webhook?
No hay un número fijo; las prácticas comunes van de 3 a 10 reintentos con retroceso exponencial. Después de eso, los eventos a menudo se envían a una cola de mensajes muertos.
¿Puedo usar webhooks sin firmas?
Es posible pero muy desaconsejado. Sin firmas, tu endpoint es vulnerable a solicitudes falsificadas y brechas de datos.
¿Listo para probar tus cargas de webhook? Usa nuestro Formateador JSON para inspeccionar y validar cargas JSON rápidamente.