mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
## Summary Fixes #24147 - Fixed EventEmitter crash when `removeAllListeners()` is called from within an event handler while a `removeListener` meta-listener is registered - Added undefined check before iterating over listeners array to match Node.js behavior - Added comprehensive regression tests ## Bug Description When `removeAllListeners(type)` was called: 1. From within an event handler 2. While a `removeListener` meta-listener was registered 3. For an event type with no listeners It would crash with: `TypeError: undefined is not an object (evaluating 'this._events')` ## Root Cause The `removeAllListeners` function tried to access `listeners.length` without checking if `listeners` was defined first. When called with an event type that had no listeners, `events[type]` returned `undefined`, causing the crash. ## Fix Added a check `if (listeners !== undefined)` before iterating, matching the behavior in Node.js core: https://github.com/nodejs/node/blob/main/lib/events.js#L768 ## Test plan - ✅ Created regression test in `test/regression/issue/24147.test.ts` - ✅ Verified test fails with `USE_SYSTEM_BUN=1 bun test` (reproduces bug) - ✅ Verified test passes with `bun bd test` (confirms fix) - ✅ Test covers the exact reproduction case from the issue - ✅ Additional tests for edge cases (actual listeners, nested calls) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com>
82 lines
2.3 KiB
TypeScript
82 lines
2.3 KiB
TypeScript
// https://github.com/oven-sh/bun/issues/24147
|
|
// EventEmitter: this._events becomes undefined when removeAllListeners()
|
|
// called from event handler with removeListener meta-listener
|
|
|
|
import { EventEmitter } from "events";
|
|
import assert from "node:assert";
|
|
import { test } from "node:test";
|
|
|
|
test("removeAllListeners() from event handler with removeListener meta-listener", () => {
|
|
const emitter = new EventEmitter();
|
|
|
|
emitter.on("test", () => {
|
|
// This should not crash even though there are no 'foo' listeners
|
|
emitter.removeAllListeners("foo");
|
|
});
|
|
|
|
// Register a removeListener meta-listener to trigger the bug
|
|
emitter.on("removeListener", () => {});
|
|
|
|
// This should not throw
|
|
assert.doesNotThrow(() => emitter.emit("test"));
|
|
});
|
|
|
|
test("removeAllListeners() with actual listeners to remove", () => {
|
|
const emitter = new EventEmitter();
|
|
let fooCallCount = 0;
|
|
let removeListenerCallCount = 0;
|
|
|
|
emitter.on("foo", () => fooCallCount++);
|
|
emitter.on("foo", () => fooCallCount++);
|
|
|
|
emitter.on("test", () => {
|
|
// Remove all 'foo' listeners while inside an event handler
|
|
emitter.removeAllListeners("foo");
|
|
});
|
|
|
|
// Track removeListener calls
|
|
emitter.on("removeListener", () => {
|
|
removeListenerCallCount++;
|
|
});
|
|
|
|
// Emit test event which triggers removeAllListeners
|
|
emitter.emit("test");
|
|
|
|
// Verify listeners were removed
|
|
assert.strictEqual(emitter.listenerCount("foo"), 0);
|
|
|
|
// Verify removeListener was called twice (once for each foo listener)
|
|
assert.strictEqual(removeListenerCallCount, 2);
|
|
|
|
// Verify foo listeners were never called
|
|
assert.strictEqual(fooCallCount, 0);
|
|
});
|
|
|
|
test("nested removeAllListeners() calls", () => {
|
|
const emitter = new EventEmitter();
|
|
const events: string[] = [];
|
|
|
|
emitter.on("outer", () => {
|
|
events.push("outer-start");
|
|
emitter.removeAllListeners("inner");
|
|
events.push("outer-end");
|
|
});
|
|
|
|
emitter.on("inner", () => {
|
|
events.push("inner");
|
|
});
|
|
|
|
emitter.on("removeListener", type => {
|
|
events.push(`removeListener:${String(type)}`);
|
|
});
|
|
|
|
// This should not crash
|
|
assert.doesNotThrow(() => emitter.emit("outer"));
|
|
|
|
// Verify correct execution order
|
|
assert.deepStrictEqual(events, ["outer-start", "removeListener:inner", "outer-end"]);
|
|
|
|
// Verify inner listeners were removed
|
|
assert.strictEqual(emitter.listenerCount("inner"), 0);
|
|
});
|