nodejs / nodejs/node

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

Ouverte
#65,637 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

Langage dominant
JavaScript
Étoiles
122k
Forks
37.3k
Merge moyen
4 j 2 h
PR mergées (30 j)
283

Description

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 {
    // ...

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Commencez par reproduire le script fourni, puis lisez lib/internal/events/abort_listener.js en vous concentrant sur la branche signal.aborted et la gestion de SymbolDispose. Ajoutez une couverture de régression pour la libération avant le microtask mis en file d’attente et pour l’argument du callback, puis vérifiez que le listener est supprimé après la libération et qu’il reçoit sinon un abort Event.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
javascript
Domaine
backend
Type d'issue
Bug
Difficulté
3/5
Temps estimé
1-2 jours
Activité
Active
Clarté
Clairement spécifiée
Accessibilité débutants
75/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.