Webhooks explicados: cargas firmadas y reintentos
Has integrado una pasarela de pago o un servicio de CI/CD, y ahora necesitas recibir eventos en tiempo real. Los webhooks son la solución estándar, pero conllevan dos grandes desafíos: verificar que la solicitud realmente proviene del remitente esperado y manejar fallos cuando tu endpoint está caído o lento. Este artículo explica cómo implementar cargas de webhook firmadas y una lógica de reintentos robusta, con ejemplos de código prácticos.
¿Qué son los webhooks?
Un webhook es una devolución de llamada HTTP definida por el usuario. En lugar de que tu aplicación consulte una API en busca de actualizaciones, el proveedor envía una solicitud HTTP POST a una URL que especifiques cada vez que ocurre un evento. Esto hace que las integraciones sean eficientes y casi en tiempo real.
Sin embargo, los webhooks se basan en push, lo que significa que tu endpoint debe ser accesible públicamente, manejar carga inesperada y lidiar con solicitudes duplicadas o maliciosas. Ahí es donde entran la firma y los reintentos.
¿Por qué firmar las cargas de webhook?
Sin verificación, cualquiera que conozca la URL de tu webhook podría enviar eventos falsos. Firmar las cargas con un secreto compartido te permite autenticar al remitente y asegurar que la carga no ha sido manipulada.
El enfoque más común es HMAC (Código de Autenticación de Mensajes basado en Hash) usando SHA-256. El proveedor calcula una firma sobre el cuerpo crudo de la solicitud usando una clave secreta y la incluye en una cabecera (por ejemplo, X-Signature). Tu servidor recalcula la firma y las compara.
Implementación de la verificación HMAC
Aquí tienes un ejemplo simple en Node.js usando Express:
const crypto = require('crypto');
const express = require('express');
const app = express();
// Usar el cuerpo crudo para la verificación de firma
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const payload = req.body; // 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');
}
// Procesar el evento
const event = JSON.parse(payload);
console.log('Received event:', event.type);
res.status(200).send('OK');
});
app.listen(3000, () => console.log('Webhook server running'));
Puntos clave:
- Usa el cuerpo crudo: Analiza el cuerpo como bytes crudos, no como JSON, porque cualquier transformación cambia la firma.
- Comparación segura en tiempo: Usa
crypto.timingSafeEqualpara prevenir ataques de temporización. - Gestión de secretos: Almacena los secretos de forma segura (variables de entorno, gestores de secretos).
Manejo de reintentos: haciendo los webhooks confiables
Las redes fallan, los servidores se caen y los despliegues causan tiempo de inactividad. Un sistema de webhooks confiable debe reintentar las entregas fallidas. Pero los reintentos ingenuos pueden causar procesamiento duplicado y sobrecargar tu sistema.
Estrategias de reintento
Los patrones de reintento comunes incluyen:
- Backoff exponencial: Espera más tiempo entre cada reintento (por ejemplo, 1s, 2s, 4s, 8s...).
- Límite máximo de reintentos: Detente después de N intentos (por ejemplo, 5) y marca la entrega como fallida.
- Jitter: Añade aleatoriedad para evitar problemas de estampida.
La mayoría de los proveedores implementan algo como: reintentar hasta 5 veces con backoff exponencial durante 24 horas.
Idempotencia: la clave para reintentos seguros
Debido a que los reintentos pueden entregar el mismo evento varias veces, tu endpoint debe ser idempotente. Incluye un ID de evento único en la carga y almacena los IDs procesados para omitir duplicados.
Ejemplo usando un conjunto simple en memoria (usa una base de datos en producción):
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
// ... verificación de firma ...
const event = JSON.parse(payload);
if (processedEvents.has(event.id)) {
return res.status(200).send('Already processed');
}
// Procesar evento
processedEvents.add(event.id);
res.status(200).send('OK');
});
Para producción, usa una base de datos con una restricción única en el ID del evento.
Mejores prácticas para consumidores de webhooks
- Responde rápidamente: Confirma la recepción con un estado 2xx antes de hacer un procesamiento pesado. Encola el evento para manejarlo de forma asíncrona.
- Valida la marca de tiempo: Si el proveedor incluye una marca de tiempo, rechaza solicitudes con más de unos minutos de antigüedad para prevenir ataques de repetición.
- Usa HTTPS: Siempre expón tu endpoint de webhook sobre HTTPS para proteger las cargas en tránsito.
- Registra todo: Registra las solicitudes entrantes, las firmas y los resultados del procesamiento para depuración.
- Monitorea los fallos: Configura alertas para fallos de entrega repetidos.
Comparación de políticas de reintento
| Política | Ventajas | Desventajas |
|---|---|---|
| Intervalo fijo | Simple de implementar | Puede sobrecargar un sistema en recuperación |
| Backoff exponencial | Reduce la carga durante caídas | Retrasa la entrega si el fallo es transitorio |
| Backoff exponencial con jitter | Previene reintentos sincronizados | Ligeramente más complejo |
Preguntas frecuentes
¿Cómo verifico una firma de webhook?
Calcula el HMAC del cuerpo crudo de la solicitud usando el secreto compartido y compáralo con la cabecera de firma usando una función segura en tiempo. Nunca compares con igualdad simple.
¿Qué debo hacer si mi endpoint de webhook está caído?
El proveedor debe reintentar con backoff exponencial. Asegúrate de que tu endpoint devuelva un 2xx rápidamente y procese los eventos de forma asíncrona para minimizar el impacto del tiempo de inactividad.
¿Puedo usar webhooks sin firma?
No se recomienda. Sin firmas, no puedes verificar al remitente, lo que hace que tu endpoint sea vulnerable a eventos falsos. Siempre usa firmas cuando estén disponibles.
Al probar cargas de webhook, es posible que necesites inspeccionar datos JSON. Usa el Formateador JSON para imprimir y validar cargas rápidamente.