Skip to content

extractBody: a byte-sequence body is enqueued again on each pull until the stream closes (since 8.12.0) #6092

Open

Description

@davi-pandektes

Bug description

Since #5926 (8.12.0), extractBody enqueues a string or byte body in pull(). Then it closes the stream in a queueMicrotask callback. If the stream calls pull() again before that callback runs, pull() enqueues the full body again.

With the real queueMicrotask, the close runs first, so the body is correct. When queueMicrotask is delayed, the reader gets the same bytes again and again. Jest fake timers fake queueMicrotask by default. In our Jest suite, MSW requests then read an infinite body, and the test process stops with "JavaScript heap out of memory".

In 8.11.2, the same setup waits for the close and does not repeat data.

Reproducible by

// npm i undici@8.12.3 @sinonjs/fake-timers@15
const FakeTimers = require("@sinonjs/fake-timers");
const { Response } = require("undici");

async function main() {
  const clock = FakeTimers.install({ toFake: ["queueMicrotask"] });
  const reader = new Response("hello").body.getReader();
  let chunks = 0;
  while (chunks < 5) {
    const { done, value } = await reader.read();
    if (done) break;
    chunks++;
    console.log(new TextDecoder().decode(value));
  }
  clock.uninstall();
  console.log(`chunks read: ${chunks}`);
}

main();

Expected behavior

The reader gets hello one time. The stream does not repeat the body, even if the close is late.

Logs & Screenshots

8.12.3 prints hello five times, then chunks read: 5. Without the limit, the loop does not stop. 8.11.2 prints hello one time.

Environment

  • Node.js 24.22.1, macOS
  • undici 8.12.0 to 8.12.3 (first bad commit by bisect: e049c53, extractBody fixes 2 #5926)
  • @sinonjs/fake-timers 15.4.0 (Jest 30 modern fake timers)

Additional context

A possible fix: keep a flag so that pull() enqueues a byte-sequence body only once, or close the stream in pull() directly after the enqueue.

Our workaround is fakeTimers: { doNotFake: ['queueMicrotask'] } in the Jest config.

Activity

  1. cestercian commented on Oct 26, 2026

    @cestercian

    I'd like to take this one. thinking pull() should only enqueue the body once (track that it was already sent) and close right away instead of depending on the queueMicrotask timing, plus a test with a delayed queueMicrotask.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions