provider.ingest(). @botmesh/server es esa capa HTTP, bien
hecha:
- Bytes crudos del body:
ingest()recibe elBufferexacto que llegó. Las firmas comoX-Hub-Signature-256de Meta se calculan sobre esos bytes, nunca sobre JSON re-serializado. - Límite de tamaño (
413), 1 MiB por defecto, comprobado contraContent-Lengthy mientras llega el stream. - Timeouts para leer el body (
408) y paraingest()(503). - Límite de concurrencia (
503+Retry-After: 1al saturarse). - Enrutado:
GET→verifyChallenge()(el handshake de Meta),POST→ingest(raw, headers, url). El valor que devuelveingest()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.
Con node:http
createNodeHandler(ingress, options) responde 404 a las rutas que no
sirve; pasa onNotFound: (req, res) => ... para encadenar otro handler.
Express
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
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
SubemaxBodyBytes 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 soloGET y HEAD (405 en otro caso):
/healthz: siempre200{"status":"ok"}mientras el proceso sirve./readyz:200cuando está listo,503si no. El body JSON es{ status: 'ready' | 'not_ready', connection?, draining? }:connectiones el estado de conexión del bot (si pasastebot) ydrainingaparece después declose().
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 aprovider.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 unRequestWHATWG.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
- Canales: qué providers funcionan por webhook.
- Despliegue
- README de
@botmesh/server