#5835·undici

`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)

Author: tomer-antCreated Sep 16, 2026Updated Sep 16, 2026

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

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

https://github.com/nodejs/undici/blob/bfea02017eca586787a62194d11cb81226a6ecc3/lib/web/fetch/body.js#L221-L243

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.