Skip to content

stream: fix addAbortSignal() for web streams#62450

Draft
ikeyan wants to merge 2 commits intonodejs:mainfrom
ikeyan:fix-k-controller-error-function
Draft

stream: fix addAbortSignal() for web streams#62450
ikeyan wants to merge 2 commits intonodejs:mainfrom
ikeyan:fix-k-controller-error-function

Conversation

@ikeyan
Copy link
Copy Markdown
Contributor

@ikeyan ikeyan commented Mar 27, 2026

This fixes stream.addAbortSignal() for web streams.

addAbortSignal() used kControllerErrorFunction, which mixed stream erroring
with abort handling.

For readable byte streams, ReadableByteStreamController left the default no-op
hook in place, so addAbortSignal() could not abort the stream.

For writable streams, WritableStreamDefaultController wired the hook to
controller.error(), so the stream became errored but
controller.signal.aborted stayed false.

This replaces kControllerErrorFunction with kControllerAbortFunction and
wires each controller to the internal operation that matches its abort path:

  • ReadableStreamDefaultController uses
    readableStreamDefaultControllerError()
  • ReadableByteStreamController uses
    readableByteStreamControllerError()
  • WritableStreamDefaultController uses writableStreamAbort()

The updated test covers addAbortSignal() on readable byte streams, aborts
that race with pending pulls, writable abort signal state, and readable and
writable controllers with overridden public methods.

@nodejs-github-bot
Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/streams

@nodejs-github-bot nodejs-github-bot added needs-ci PRs that need a full CI run. stream Issues and PRs related to the stream subsystem. web streams labels Mar 27, 2026
Comment on lines +1325 to +1327
stream[kControllerAbortFunction] = (reason) => {
setPromiseHandled(writableStreamAbort(stream, reason));
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly off-topic: should we make this a class method instead? For WritableStream, we could do:

class WritableStream {
  [kControllerAbortFunction](reason) {
    setPromiseHandled(writableStreamAbort(this, reason));
  }
}

For ReadableStream, the method could check the type of this[kState].controller and then call either readableStreamDefaultControllerError or readableByteStreamControllerError.

stream[kState].controller = controller;
stream[kControllerErrorFunction] = FunctionPrototypeBind(controller.error, controller);
stream[kControllerAbortFunction] =
(error) => readableStreamDefaultControllerError(controller, error);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity: why are we erroring the stream instead of canceling it?

  • A consumer can cancel the stream with readableStreamCancel, and the stream will call the source.cancel callback so it can clean up any resources.
  • With controller.error, there is no so such callback, since the Streams standard assumes that the error was signaled by the source. If we allow a consumer to inject an error into the stream, this may leak resources in the source.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MattiasBuelens
Should I include that fix here as well?

Copy link
Copy Markdown
Contributor

@MattiasBuelens MattiasBuelens Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that's probably not a good idea. The addAbortSignal documentation currently states that it calls controller.error, so changing that will be a breaking change... 🤔

Let's stick with the current behavior for now. I'll put up a separate draft PR to discuss if we want to change it or not.

Actually, this PR is already introducing a breaking change for WritableStream... so maybe we should just fix both. 😅

Copy link
Copy Markdown
Contributor

@MattiasBuelens MattiasBuelens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is a breaking change for WritableStreams.

I think this PR is a good change: using controller.error from a consumer API is fundamentally wrong, and we got this wrong from the start in #46273. But that does mean this PR is a semver-major change.

While we're at it, I think we should fix the behavior for ReadableStreams too to call readableStreamCancel instead.

@nodejs/streams Thoughts?

stream[kState].controller = controller;
stream[kControllerErrorFunction] = FunctionPrototypeBind(controller.error, controller);
stream[kControllerAbortFunction] = (reason) => {
setPromiseHandled(writableStreamAbort(stream, reason));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change. From doc/api/stream.md:

Calling abort on the AbortController corresponding to the passed
AbortSignal will behave the same way as calling .destroy(new AbortError())
on the stream, and controller.error(new AbortError()) for webstreams.

We'll need to at least update the docs to document the new behavior.

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

Labels

needs-ci PRs that need a full CI run. stream Issues and PRs related to the stream subsystem. web streams

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants