Saltar al contenido principal
Los providers por webhook (WhatsApp Cloud API, Messenger, Instagram, Telegram en modo webhook, Evolution, Twilio, TikTok…) no abren ninguna conexión: el canal envía los eventos por POST a tu servidor HTTP, y tú le pasas cada petición a provider.ingest(). @botmesh/server es esa capa HTTP, bien hecha:
  • Bytes crudos del body: ingest() recibe el Buffer exacto que llegó. Las firmas como X-Hub-Signature-256 de Meta se calculan sobre esos bytes, nunca sobre JSON re-serializado.
  • Límite de tamaño (413), 1 MiB por defecto, comprobado contra Content-Length y mientras llega el stream.
  • Timeouts para leer el body (408) y para ingest() (503).
  • Límite de concurrencia (503 + Retry-After: 1 al saturarse).
  • Enrutado: GET → verifyChallenge() (el handshake de Meta), POST → ingest(raw, headers, url). El valor que devuelve ingest() es el status HTTP.
  • Endpoints de salud: /healthz (liveness) y /readyz (readiness).
  • Sin logs sensibles: solo método, ruta, status, tamaños y el nombre de la clase de error; nunca bodies, cabeceras, query strings ni mensajes de error.
  • Sin dependencias de frameworks: adaptadores para node:http, Express, Fastify, Hono y runtimes estilo fetch, tipados estructuralmente.
Es la forma recomendada de alojar los canales por webhook. No tiene dependencias:
Los providers por socket (Baileys, Discord, Telegram en modo polling, Email por IMAP) no necesitan ingress.

Con node:http

createNodeHandler(ingress, options) responde 404 a las rutas que no sirve; pasa onNotFound: (req, res) => ... para encadenar otro handler.

Express

Las rutas que no sirve el ingress pasan a next(). Las rutas se comparan con req.url, así que son relativas a donde montes el middleware. Si un parser ya consumió el body (express.json() montado antes), el ingress responde 500 en lugar de re-serializar. Detrás de express.raw() reutiliza ese Buffer (y sigue aplicando maxBodyBytes).

Fastify

Es un plugin encapsulado: su parser comodín deja el stream intacto solo para sus propias rutas; el resto de tus rutas conserva el parser JSON de Fastify.

Hono y runtimes fetch

serveFetch(ingress, request) es la variante de más bajo nivel: resuelve undefined cuando ninguna ruta coincide, para que puedas seguir con otra cosa. El adaptador de Hono necesita Buffer (Node, Bun, Deno, Workers con nodejs_compat).

Opciones

createIngress(options): Reglas de rutas: path debe empezar por /, la barra final se ignora, y las rutas duplicadas (una ruta de webhook igual a una de salud, o healthPath igual a readyPath) lanzan un TypeError. También lanza una ruta cuyo provider no tiene método ingest() o cuyo body no es 'buffer' ni 'text', igual que un límite no positivo, un timeout por encima de 2147483647 ms (el mayor retardo que respeta setTimeout; uno mayor saltaría a 1 ms), un bot sin getConnectionState() y un readiness que no es una función. Todos los mensajes empiezan por @botmesh/server: y nombran la opción. Los valores por defecto se exportan como DEFAULT_MAX_BODY_BYTES, DEFAULT_BODY_TIMEOUT_MS, DEFAULT_INGEST_TIMEOUT_MS y DEFAULT_MAX_CONCURRENT.

body: 'buffer' o 'text'

ingest() recibe el body crudo como Buffer (por defecto) o como string UTF-8 con body: 'text'. TelegramProvider acepta los dos (sus primeras alphas necesitaban body: 'text').

Límite de tamaño por ruta

Sube maxBodyBytes en las rutas que lo necesiten, por ejemplo el MIME crudo para EmailProvider.ingest():

Respuestas

EmailProvider.ingest() resuelve { status, accepted, reason? }, así que se usa su status (200, 400 o 413) tal cual. Con node:http, Express y Fastify, antes de responder con Connection: close (413, 503…) se descarta el resto del body sin leer para que el cliente reciba la respuesta en lugar de un ECONNRESET. El drenado está acotado (8 MiB y 5 s por defecto; createNodeHandler(ingress, { drain: { maxBytes, timeoutMs } })); al superarlo se responde igualmente y se corta el socket. Un 408 no espera al body.

Endpoints de salud

Ambos aceptan solo GET y HEAD (405 en otro caso):
  • /healthz: siempre 200 {"status":"ok"} mientras el proceso sirve.
  • /readyz: 200 cuando está listo, 503 si no. El body JSON es { status: 'ready' | 'not_ready', connection?, draining? }: connection es el estado de conexión del bot (si pasaste bot) y draining aparece después de close().
Listo significa: no se llamó a close(), el bot (si lo hay) está en 'ready' y readiness() (si lo hay) resolvió true.

El objeto Ingress

close() sin timeoutMs espera hasta que terminen todas las peticiones en curso; un timeoutMs fuera de 0-2147483647 rechaza con un TypeError. close() no tiene vuelta atrás: crea otro ingress para volver a servir.

Notas por provider

El ingress llama a provider.ingest(rawBody, headers, requestUrl), donde requestUrl es la ruta de la petición con su query string (/hook?token=…), tal como llegó (sin recodificar, así una firma sobre la URL como la de Twilio sigue cuadrando). Los providers que autentican un token en la URL lo leen de ahí: Sin el ingress, monta una ruta en tu framework y pasa tú la URL: provider.ingest(body, headers, req.originalUrl) (WPPConnect, Gupshup, y Twilio con webhookPath) o provider.ingestRequest({ url, body, headers }) (Gupshup, y Twilio con la URL pública completa) (ver Canales).

Exports de más bajo nivel

  • serveNode(ingress, req, res, options): sirve una petición de Node (lo usan los adaptadores).
  • serveFetch(ingress, request): sirve un Request WHATWG.
  • readNodeBody(req, limit, timeoutMs) / readWebBody(request, limit, timeoutMs): leen un body crudo con límite de bytes y timeout.
  • BodyTooLargeError, BodyTimeoutError, BodyUnavailableError, BodyAbortedError: los errores con los que rechazan esos lectores.
  • normalizePath(path), splitUrl(url): helpers de rutas, en @botmesh/server/internal (sin garantía semver).

Ver también