Feedjoltdocs
DesarrolladoresWebhooks

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=false

Entrega 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
  });
}

On this page