[BUG] downloadEncryptedContent: a mid-download connection drop throws an unhandled stream 'error' (uncaughtException) and the returned stream never ends
Describe the bug
downloadEncryptedContent ends with:
return fetched.pipe(output, { end: true })fetched is Readable.fromWeb(response.body) from getHttpStream, and no 'error' listener is ever attached to it. .pipe() does not forward source errors to the destination, so when a media download is cut mid-body (undici raises TypeError: terminated) two things happen:
- The
'error'event onfetchedhas no listener, so Node re-throws it — the host application gets a process-leveluncaughtExceptionwhose stack contains only undici internals, with nothing pointing at Baileys or at the caller:
TypeError: terminated
at Fetch.onAborted (node:internal/deps/undici/undici:11457:53)
at Fetch.emit (node:events:519:28)
at Fetch.terminate (node:internal/deps/undici/undici:10615:14)
...- Worse:
outputis never ended and never errored, so the returned stream stays open forever.await downloadMediaMessage(msg, 'buffer')never settles — it does not resolve and does not reject. Atry/catcharound the call never runs, and anyfor await (const chunk of stream)hangs indefinitely.
For a long-running process this means a messages.upsert handler can be wedged permanently by one flaky download, silently losing the remaining messages in that batch. It looks like a crash in the logs while actually being a silent stall.
To Reproduce
Self-contained, no WhatsApp connection needed — a local server that promises more bytes than it sends and then resets:
const http = require('http')
const crypto = require('crypto')
const { downloadEncryptedContent } = require('baileys')
process.on('uncaughtException', e => console.log('UNCAUGHT:', e.name + ':', e.message))
const server = http.createServer((req, res) => {
res.writeHead(200, { 'content-length': '100000' }) // promise 100kB
res.write(crypto.randomBytes(4096)) // send 4kB
setTimeout(() => res.socket.destroy(), 50) // then reset
})
server.listen(0, async () => {
const url = `http://127.0.0.1:${server.address().port}/media.enc`
const keys = { cipherKey: crypto.randomBytes(32), iv: crypto.randomBytes(16) }
const stream = await downloadEncryptedContent(url, keys, {})
let settled = 'NEVER SETTLED'
;(async () => {
try { for await (const _ of stream) {} settled = 'ended' }
catch (e) { settled = 'threw ' + e.message }
})()
setTimeout(() => { console.log('consumer settled?', settled); process.exit(0) }, 2000)
})Output:
UNCAUGHT: TypeError: terminated
consumer settled? NEVER SETTLEDExpected behavior
The error should propagate to the consumer — downloadMediaMessage(...) / iterating the returned stream should reject, so the caller's try/catch can treat it as a failed download and move on. Nothing should reach process.on('uncaughtException').
Suggested fix
One line, at src/Utils/messages-media.ts:652:
fetched.on('error', err => output.destroy(err))
return fetched.pipe(output, { end: true })or use stream.pipeline(fetched, output, () => {}), which handles this (and cleans up the source) by design.
With that, the same repro rejects with TypeError: terminated at the consumer instead of crashing the process, and callers can retry or give up as they see fit.
Environment
- Baileys
7.0.0-rc13(line is unchanged onmasteras of today:src/Utils/messages-media.ts:652) - Node.js v22.14.0 and v26.5.0 — same behaviour on both
- Reproduced with the real library, not a mock
Happy to open a PR with the one-liner if that's welcome.
Source: WhiskeySockets/Baileys