`FormData` body with an errored `Blob` part crashes the process with an unhandled rejection instead of erroring the body stream (regression in 7.20.0)
Bug Description
When a FormData body contains a Blob whose stream() errors, such as a file-backed Blob from fs.openAsBlob() whose file has since changed, reading the body crashes the process with an unhandled rejection instead of rejecting the read. With --unhandled-rejections=warn the read never settles. A fetch() with such a body crashes the process instead of rejecting with TypeError: fetch failed.
This regressed in 7.20.0 (#4791). 7.19.2 rejects as expected. 7.29.0, 8.10.0, and main are still affected.
Reproduction
'use strict'
const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const { FormData, Response } = require('undici')
async function main () {
const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'undici-')), 'a')
fs.writeFileSync(file, 'hello')
const form = new FormData()
form.append('file', await fs.openAsBlob(file), 'a')
fs.appendFileSync(file, ' more') // invalidates the file-backed Blob, so its stream() errors with NotReadableError
try {
await new Response(form).text()
console.log('resolved')
} catch (err) {
console.log('rejected:', err.name)
}
}
main()No server is needed. fetch(url, { method: 'POST', body: form }) against a local createServer also crashes the process, whereas undici 6.x rejects with TypeError: fetch failed (cause: NotReadableError).
Expected Behavior
rejected: NotReadableError and exit code 0. 7.19.2 prints this, and reading form.get('file').stream() directly rejects with the same error.
Actual Behavior
On 7.20.0, 7.29.0, and 8.10.0 neither console.log runs and the process exits with code 1. Output with 8.10.0:
node:internal/blob:505
lazyDOMException('The blob could not be read',
^
DOMException [NotReadableError]: The blob could not be read
at BlobReader.<anonymous> (node:internal/blob:505:13)
at Object.readNext (node:internal/blob:484:14)
at Object.pull (node:internal/blob:480:12)
at Object.<anonymous> (node:internal/webstreams/util:190:25)
at readableByteStreamControllerCallPullIfNeeded (node:internal/webstreams/readablestream:3369:24)
at readableByteStreamControllerPullSteps (node:internal/webstreams/readablestream:3472:3)
at [kPull] (node:internal/webstreams/readablestream:1291:5)
at readableStreamDefaultReaderRead (node:internal/webstreams/readablestream:2445:39)
at nextSteps (node:internal/webstreams/readablestream:519:7)
at async action (node_modules/undici/lib/web/fetch/body.js:162:11)
Node.js v24.19.0With node --unhandled-rejections=warn --no-warnings repro.js there is no output and the exit code is 0, because the text() promise never settles.
Environment
- OS: macOS 26.6.2 (arm64)
- Node.js version: v24.19.0 (also reproduced on v22.22.3 with the npm package)
- undici version: 7.20.0, 7.29.0, 8.10.0 fail; 7.19.2 passes. Node's bundled undici is affected from Node v24.14.0 (undici 7.21.0) onward. Node 22.x (undici 6.x) is unaffected.
Additional context
Cause: #4791 replaced the pull-based ReadableStream in extractBody with an async IIFE that iterates action() and enqueues into controller:
Nothing awaits the IIFE and it has no catch, so when yield * part.stream() throws, the rejection goes unhandled and controller is never errored. Before #4791, pull() returned iterator.next(), so a rejection errored the stream.
I confirmed this on 8.10.0 by wrapping the IIFE body in try { … } catch (err) { controller.error(err) }, after which the script above prints rejected: NotReadableError.
Source: nodejs/undici