events: addAbortListener ignores disposal and passes undefined event when signal is already aborted
Nessuno ha ancora preso questa issue.
- Lingua principale
- JavaScript
- Stelle
- 122k
- Fork
- 37.3k
- Merge medio
- 4g 2h
- PR unite (30g)
- 283
Descrizione
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?
- Calling
disposable[Symbol.dispose]()(or exiting ausingscope) before the microtask executes should disarm/cancel the scheduled listener. The main motivation foraddAbortListener()is TC39 explicit resource management, so disposal should always prevent the callback from firing. - The listener callback should receive an
Eventobject (type === 'abort'), consistent with when the listener is triggered by an active signal.
What do you see instead?
- The listener runs unconditionally in the microtask, ignoring
Symbol.dispose(). - The listener receives
undefinedinstead of anEventinstance. Code following the documented(e) => { ... }signature that accesses properties likee.typecrashes with aTypeErrorinside 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:
removeEventListeneris leftundefined, makingSymbolDisposea no-op.queueMicrotask(() => listener())invokeslistenerwithout passing anEventobject.
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 {
// ...
Guida per i contributori
Apri la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Direzione di ricerca
Inizia riproducendo lo script fornito, quindi leggi lib/internal/events/abort_listener.js, concentrandoti sul ramo signal.aborted e sulla gestione di SymbolDispose. Aggiungi una copertura di regressione per il rilascio prima del microtask accodato e per l’argomento del callback, quindi verifica che il listener venga soppresso dopo il rilascio e che altrimenti riceva un abort Event.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- javascript
- Ambito
- backend
- Tipo di issue
- Bug
- Difficoltà
- 3/5
- Tempo stimato
- 1-2 giorni
- Stato di attività
- Attiva
- Chiarezza
- Specificata chiaramente
- Idoneità per principianti
- 75/100