El webhook de Postiz que no pude construir, y el sondeo en su lugar
El gestor de blog programa publicaciones sociales a través de Postiz, un programador autoalojado. Programar es la mitad fácil. La mitad difícil es descubrir, después, dónde terminó realmente la publicación, Postiz publica de forma asíncrona, así que la llamada de “schedule” retorna antes de que algo esté en vivo y sin ningún enlace a la publicación ya publicada. El plan sobre el papel era simple: recibir el webhook de publicación de Postiz y registrar la URL. Ese receptor nunca se escribió, y la razón por la que murió es más interesante que el código que lo reemplazó.
Leer antes de construir
Postiz es de código abierto, así que en vez de adivinar el payload del webhook, fijé mi lectura a la versión exacta que corre en mi servidor (v2.21.8) y rastreé el camino de publicación. Dos hallazgos, cualquiera de los dos era fatal.
No acepta mi URL. La configuración del webhook valida el destino con una función llamada IsSafeWebhookUrl. Hace una búsqueda DNS real y rechaza cualquier cosa que resuelva a una dirección privada, 10.x, 192.168.x, y así. El gestor de blog vive en un dominio del homelab que resuelve a una dirección privada 192.168.x.x. Postiz se niega a registrarla. No hay ningún endpoint público al cual apuntar, así que ni siquiera es posible crear el webhook.
Y de todas formas el payload está vacío. Incluso dejando de lado el problema de la URL, el webhook de v2.21.8 no tiene autenticación (sin secreto, sin firma, no habría forma de saber si un POST viene realmente de Postiz), solo se dispara en caso de éxito (así que los fallos son invisibles), y, por un desajuste de id en el código fuente, envía un array vacío por POST. La función que arma el cuerpo busca la publicación con el id equivocado, no encuentra nada, y manda []. Lo rastreé línea por línea y todavía esperaba a medias estar equivocado, pero el camino es inequívoco.
Así que la función tal como estaba especificada era imposible de construir. Es un resultado útil: un spike que termina en “no construir esto” evitó levantar un ingress público y un receptor para atrapar pings vacíos e imposibles de verificar.
Sondeo, la respuesta aburrida que funciona
Lo bueno de un sistema asíncrono con un webhook defectuoso es que, en general, se le puede simplemente preguntar. Postiz tiene un endpoint de lectura, GET /posts sobre un rango de fechas, y al llamarlo con mi clave de API, ahí estaba cada campo que el webhook debía entregar: por canal, el estado (QUEUE / PUBLISHED / ERROR), la URL publicada, el nombre del proveedor. Autenticado, completo, e incluso informa fallos que el webhook nunca habría mostrado.
Así que se hace sondeo. Cuando se programa una publicación, se captura el id de publicación por canal que devuelve Postiz, y un job en segundo plano revisa el calendario un minuto después, compara esos ids, y registra dónde terminó cada canal, cambiando la publicación a “posted” una vez que todos los canales están en vivo, con reintentos espaciados mientras alguno siga en cola. Menos elegante que un push, pero del tipo de menos elegante que realmente funciona.
La trampa: publicaciones de antes de capturar ids
Publiqué la captura de ids y la probé, y una publicación de más temprano ese mismo día seguía marcada como “scheduled.” Claro que sí, había salido antes de que existiera el código que captura el id de correlación, así que el sondeo no tenía nada contra qué comparar. En vez de adivinar, revisé qué devolvía realmente el calendario y encontré la URL de la publicación justo ahí en el contenido de la fila. Así que se le dio al sondeo un respaldo: si no hay id capturado, empareja con la fila de Postiz cuyo contenido contiene la URL de la publicación. Las publicaciones nuevas emparejan por id, las viejas se autorreparan por URL. Una pasada de reconciliación y todo el catálogo anterior se iluminó con sus enlaces reales.
Qué me llevo
Dos cosas. Primero, conviene leer el código fuente de lo que se está integrando antes de diseñar en base a su documentación o su rastreador de issues, el webhook “existe” en el sentido de que hay código, y “no funciona” en el sentido de que el código manda un cuerpo vacío, y solo el código fuente dice cuál de las dos es. Segundo, cuando una API push falla, conviene revisar si el mismo sistema tiene una API de lectura (pull). Casi siempre la tiene, y suele ser la que de verdad se mantiene.
Lecturas relacionadas
Add a feature, or move a responsibility?
Adding Postiz social cross-posting looked done until a blunt question exposed a double-post bug, and a full audit of every posting path in the app found two more like it.
Programación y enlaces más inteligentes para mi publicador cruzado de Postiz
Programación a futuro, descripciones más completas con miniaturas, y un selector de qué URL usar, más el error de zona horaria del reloj de pared y la importación que copió la columna equivocada.
El último publicador nativo: terminando la consolidación de Postiz
La integración nativa de Medium era una pieza de museo. Un puente de navegador, un artículo cuyo cuerpo es una URL, y menos 1.603 líneas.