fix(whatsapp): fail fast when Baileys sendMessage hangs

Baileys' sock.sendMessage() can hang indefinitely while uploading
media to WhatsApp servers (and, less often, on text sends), pinning
the bridge's Express handler until the gateway's aiohttp timeout
fires — surfacing to the user as a 120s wait followed by an empty
error from the TTS/voice path.

Wrap every sock.sendMessage() call inside the bridge in a
sendWithTimeout() helper that rejects after WHATSAPP_SEND_TIMEOUT_MS
(default 60s) via Promise.race. The four call sites are /send,
/edit, and /send-media's primary send. Express handlers catch the
rejection in their existing try/catch and return a real 500 to the
gateway, which can then surface a retryable error.

Salvaged from #2608 — wysie diagnosed the hang and the
Promise.race shape; the other two parts of that PR (gateway HTTP
session pooling, base.py metadata kwarg removal) already landed on
main via separate routes and are no longer needed.

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
This commit is contained in:
Wysie 2026-05-15 01:29:43 -07:00 committed by Teknium
parent 0161d4bb6c
commit 681778a0b7

View file

@ -57,11 +57,28 @@ const REPLY_PREFIX = process.env.WHATSAPP_REPLY_PREFIX === undefined
: process.env.WHATSAPP_REPLY_PREFIX.replace(/\\n/g, '\n');
const MAX_MESSAGE_LENGTH = parseInt(process.env.WHATSAPP_MAX_MESSAGE_LENGTH || '4096', 10);
const CHUNK_DELAY_MS = parseInt(process.env.WHATSAPP_CHUNK_DELAY_MS || '300', 10);
// Per-call timeout for sock.sendMessage(). Baileys occasionally hangs forever
// when uploading media to WhatsApp servers (and, less often, on text sends),
// which pins the bridge's HTTP handler until the upstream aiohttp timeout
// fires. Fail fast instead so the gateway can surface a real error and retry.
const SEND_TIMEOUT_MS = parseInt(process.env.WHATSAPP_SEND_TIMEOUT_MS || '60000', 10);
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function sendWithTimeout(chatId, payload, timeoutMs = SEND_TIMEOUT_MS) {
let timer;
const timeoutPromise = new Promise((_, reject) => {
timer = setTimeout(
() => reject(new Error(`sendMessage timed out after ${timeoutMs / 1000}s`)),
timeoutMs,
);
});
return Promise.race([sock.sendMessage(chatId, payload), timeoutPromise])
.finally(() => clearTimeout(timer));
}
function formatOutgoingMessage(message) {
// In bot mode, messages come from a different number so the prefix is
// redundant — the sender identity is already clear. Only prepend in
@ -487,7 +504,7 @@ app.post('/send', async (req, res) => {
const chunks = splitLongMessage(formatOutgoingMessage(message));
const messageIds = [];
for (let i = 0; i < chunks.length; i += 1) {
const sent = await sock.sendMessage(chatId, { text: chunks[i] });
const sent = await sendWithTimeout(chatId, { text: chunks[i] });
trackSentMessageId(sent);
if (sent?.key?.id) messageIds.push(sent.key.id);
if (chunks.length > 1 && i < chunks.length - 1) {
@ -521,10 +538,10 @@ app.post('/edit', async (req, res) => {
const chunks = splitLongMessage(formatOutgoingMessage(message));
const messageIds = [];
await sock.sendMessage(chatId, { text: chunks[0], edit: key });
await sendWithTimeout(chatId, { text: chunks[0], edit: key });
if (chunks.length > 1) {
for (let i = 1; i < chunks.length; i += 1) {
const sent = await sock.sendMessage(chatId, { text: chunks[i] });
const sent = await sendWithTimeout(chatId, { text: chunks[i] });
trackSentMessageId(sent);
if (sent?.key?.id) messageIds.push(sent.key.id);
if (i < chunks.length - 1) {
@ -625,7 +642,7 @@ app.post('/send-media', async (req, res) => {
break;
}
const sent = await sock.sendMessage(chatId, msgPayload);
const sent = await sendWithTimeout(chatId, msgPayload);
trackSentMessageId(sent);