Reintentos e idempotencia de webhooks
Cómo gestiona Feedjolt los fallos de entrega de webhooks hoy: un intento, registros, reenvío manual y cómo crear handlers idempotentes y seguros ante desorden.
La entrega es disparar y registrar: hacemos POST de tu payload una vez. Si tu endpoint devuelve no-2xx o hace timeout, el fallo se registra y un admin puede replayar la entrega desde el panel.
Lo que hacemos hoy
- Un intento de entrega por evento, con un timeout de 10 segundos.
- Cada intento se registra en
Ajustes -> Webhooks -> [endpoint] -> Entregas- código de estado, cuerpo de respuesta (limitado a 4 KB), bandera de éxito/fallo. - Replay manual: cada fila de log tiene un botón Replay. Pulsarlo reencola el mismo payload. Útil para "arreglé el bug, reprocesa los eventos de ayer".
- Los endpoints se pueden desactivar (
is_active = false) manualmente en el panel.
Lo que NO hacemos (todavía)
- Sin agenda de reintentos automática. Un solo fallo transitorio significa que no ves ese evento sin un replay manual.
- Sin desactivación automática de endpoints poco saludables.
- Sin política de retención de logs - los logs se conservan indefinidamente (esto cambiará probablemente).
- Sin protección contra replay en la propia firma. Ver Firma -> Protección contra replay.
Están en la hoja de ruta. Hasta que lleguen, los defaults seguros van abajo.
Comportamiento recomendado del cliente
Haz tu handler idempotente
El mismo evento lógico puede ser replayado manualmente. Usa el campo id del payload (cuando esté presente - es estable entre replays del mismo evento) como clave de deduplicación:
async function handle(event: any) {
if (!event.id) {
// eventos antiguos sin id - procesa best-effort
await processEvent(event);
return;
}
const seen = await redis.get(`feedjolt:event:${event.id}`);
if (seen) return;
await processEvent(event);
await redis.set(`feedjolt:event:${event.id}`, "1", "EX", 30 * 24 * 60 * 60);
}Un TTL de 30 días te da margen generoso para replays manuales de eventos antiguos.
Para procesado respaldado por base de datos, una restricción UNIQUE en event_id consigue lo mismo con garantías más fuertes:
CREATE TABLE feedjolt_events (
event_id TEXT PRIMARY KEY,
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
payload JSONB NOT NULL
);
INSERT INTO feedjolt_events (event_id, payload)
VALUES ($1, $2)
ON CONFLICT (event_id) DO NOTHING;Responde rápido, encola el trabajo pesado
Tenemos un timeout de 10 segundos. Un handler que tarda 8 segundos funciona; uno que tarda 12 se registra como fallo. Empuja el trabajo costoso a una cola en background y devuelve 200 al instante.
No 5xx ante bugs de aplicación
Si tu BD está caída, un 503 es justo. Si el evento está mal formado (según tus reglas de aplicación), responde 200 y registra - no hay reintento automático que mantenga el evento roto fuera de tu cola.
Vigila el log de entregas
Hasta que llegue la desactivación automática, el log de entregas es tu alarma. Un job semanal sencillo que escanee la tasa de fallos por endpoint atrapará problemas antes de que se acumulen. Lo exponemos vía API:
GET /api/v1/webhooks/{endpoint_id}/deliveries?success=falseEntrega fuera de orden
No garantizamos el orden de los eventos. Dos eventos status.changed para el mismo post pueden llegar en cualquier orden si los cambios subyacentes ocurrieron con segundos de diferencia.
Ignora eventos viejos usando la marca de tiempo del objeto post dentro del payload:
async function handleStatusChanged(event: any) {
const post = await db.posts.find(event.post.id);
if (post.last_status_event_at && new Date(event.post.updated_at) < post.last_status_event_at) {
return;
}
await db.posts.update(event.post.id, {
status: event.to_status.name,
last_status_event_at: event.post.updated_at
});
}Firma de webhooks
Verifica webhooks de Feedjolt con HMAC-SHA256 sobre el cuerpo en bruto. Formato de cabecera, algoritmo de verificación, código para Node, Python, Ruby y Go, y errores.
API
Una visión general de la API REST de Feedjolt: JSON sobre HTTPS con token bearer. Cubre la URL base, la referencia interactiva, convenciones y versión v1.
