nodejs / nodejs/node

events: addAbortListener ignores disposal and passes undefined event when signal is already aborted

Đang mở
#65,637 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Ngôn ngữ chính
JavaScript
Star
122k
Fork
37.3k
Merge trung bình
4 ngày 2 giờ
Pull request đã merge (30 ngày)
283

Mô tả

Version

Tested on v24.7.0, reproduces on main (commit a48e33fb6f).

Platform
All platforms (pure JS logic in `lib/internal/events/abort_listener.js`).
Subsystem

events

What steps will reproduce the bug?
'use strict';
const { addAbortListener } = require('node:events');

// --- Bug 1: Disposing the listener does not cancel it if the signal was already aborted ---
const ac = new AbortController();
ac.abort();

let listenerCalledAfterDispose = false;
{
  using _ = addAbortListener(ac.signal, (event) => {
    listenerCalledAfterDispose = true;
    console.log('Event argument received:', event); // Bug 2: logs 'undefined' instead of an Event object
  });
  // Scope exits here -> disposable[Symbol.dispose]() is invoked synchronously.
}

queueMicrotask(() => {
  console.log('Listener called after disposal:', listenerCalledAfterDispose);
  // Prints: true (Expected: false)
});
How often does it reproduce? Is there a required condition?

100% reproducible whenever addAbortListener() is called on an AbortSignal where signal.aborted === true.

What is the expected behavior? Why is that the expected behavior?
  1. Calling disposable[Symbol.dispose]() (or exiting a using scope) before the microtask executes should disarm/cancel the scheduled listener. The main motivation for addAbortListener() is TC39 explicit resource management, so disposal should always prevent the callback from firing.
  2. The listener callback should receive an Event object (type === 'abort'), consistent with when the listener is triggered by an active signal.
What do you see instead?
  1. The listener runs unconditionally in the microtask, ignoring Symbol.dispose().
  2. The listener receives undefined instead of an Event instance. Code following the documented (e) => { ... } signature that accesses properties like e.type crashes with a TypeError inside the microtask.
Additional information

In lib/internal/events/abort_listener.js:

  let removeEventListener;
  if (signal.aborted) {
    queueMicrotask ??= require('internal/process/task_queues').queueMicrotask;
    queueMicrotask(() => listener()); // <-- 1. Listener called with no argument
  } else {
    kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
    abortListenerOptions ??= ObjectFreeze({ __proto__: null, once: true, [kResistStopPropagation]: true });
    signal.addEventListener('abort', listener, abortListenerOptions);
    removeEventListener = () => {
      signal.removeEventListener('abort', listener);
    };
  }
  return {
    __proto__: null,
    [SymbolDispose]() {
      removeEventListener?.(); // <-- 2. removeEventListener is undefined on the aborted branch
    },
  };

When signal.aborted is true:

  • removeEventListener is left undefined, making SymbolDispose a no-op.
  • queueMicrotask(() => listener()) invokes listener without passing an Event object.

A possible fix is tracking a disposed flag for the microtask and creating an Event('abort') instance:

  let removeEventListener;
  if (signal.aborted) {
    queueMicrotask ??= require('internal/process/task_queues').queueMicrotask;
    let disposed = false;
    queueMicrotask(() => {
      if (!disposed) {
        const { Event } = require('internal/event_target');
        listener(new Event('abort', { cancelable: false, bubbles: false }));
      }
    });
    removeEventListener = () => {
      disposed = true;
    };
  } else {
    // ...

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu bằng cách tái hiện script được cung cấp, sau đó đọc lib/internal/events/abort_listener.js, tập trung vào nhánh signal.aborted và việc xử lý SymbolDispose. Thêm kiểm thử hồi quy cho việc disposal trước microtask đã được xếp hàng và cho đối số callback, rồi xác minh rằng listener bị ngăn sau khi disposal và nếu không thì nhận được một abort Event.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
javascript
Lĩnh vực
backend
Loại issue
Lỗi
Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức độ hoạt động
Sôi nổi
Độ rõ ràng
Đặc tả rõ ràng
Mức phù hợp với người mới
75/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.