`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)
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 7.7k
- Forks
- 879
- Avg merge
- 2d 16h
- Merged PRs (30d)
- 68
Description
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.0
With 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with lib/web/fetch/body.js around the async body-processing code at lines 221-243, then run the supplied FormData and file-backed Blob reproduction. Done means Response(form).text() rejects with NotReadableError and a fetch with the same body rejects instead of crashing or hanging.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100