#2750·Baileys

[BUG] downloadEncryptedContent: a mid-download connection drop throws an unhandled stream 'error' (uncaughtException) and the returned stream never ends

Author: ShmeldronCreated Aug 4, 2026Updated Sep 17, 2026
LabelsStale

Describe the bug

downloadEncryptedContent ends with:

javascript
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:

  1. The 'error' event on fetched has no listener, so Node re-throws it — the host application gets a process-level uncaughtException whose 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)
    ...
  1. Worse: output is 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. A try/catch around the call never runs, and any for 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:

javascript
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 SETTLED

Expected 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:

javascript
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 on master as 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.