nodejs / nodejs/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)

Open Beginner friendly
#5,835 0 comments 0 reactions 0 assignees View on GitHub

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:

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.