Skip to content

stream/iter: abort signal causes broadcast() and share() consumers to resolve EOF instead of rejecting #63357

@trivikr

Description

@trivikr

Version

26.1.0

Platform

macOS 26.5.0

Subsystem

stream

What steps will reproduce the bug?

import { broadcast, share } from 'node:stream/iter';

const outcome = (p) => p.then(
  (value) => ({ status: 'resolved', value }),
  (err) => ({ status: 'rejected', name: err.name, message: err.message }),
);

// broadcast({ signal })
{
  const ac = new AbortController();
  const { broadcast: bc } = broadcast({ signal: ac.signal });
  const pending = outcome(bc.push()[Symbol.asyncIterator]().next());

  ac.abort();

  console.log('broadcast:', await pending);
}

// share(source, { signal })
{
  const ac = new AbortController();
  const enc = new TextEncoder();

  async function* source() {
    yield [enc.encode('a')];
    yield [enc.encode('b')];
  }

  const shared = share(source(), {
    highWaterMark: 1,
    backpressure: 'block',
    signal: ac.signal,
  });

  const fast = shared.pull()[Symbol.asyncIterator]();
  shared.pull(); // Lagging consumer keeps the one-slot buffer full.

  await fast.next();
  const pending = outcome(fast.next());

  ac.abort();

  console.log('share:', await pending);
}

Run with

node --experimental-stream-iter repro.mjs

How often does it reproduce? Is there a required condition?

Always

What is the expected behavior? Why is that the expected behavior?

broadcast: {
  status: 'rejected',
  name: 'AbortError',
  message: 'This operation was aborted'
}
share: {
  status: 'rejected',
  name: 'AbortError',
  message: 'This operation was aborted'
}

Both should reject with signal.reason, which is the default AbortError from AbortController.abort().

What do you see instead?

broadcast: {
  status: 'resolved',
  value: [Object: null prototype] { done: true, value: undefined }
}
share: {
  status: 'resolved',
  value: [Object: null prototype] { done: true, value: undefined }
}

Both pending consumers resolve with { done: true, value: undefined }, treating abort as clean EOF.

Additional information

No response

Metadata

Metadata

Assignees

Labels

streamIssues and PRs related to the stream subsystem.

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions