Files
bun.sh/test/regression/issue/24147.test.ts
robobun 4f1b90ad1d Fix EventEmitter crash in removeAllListeners with removeListener meta-listener (#24148)
## 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>
2025-10-28 12:32:15 -07:00

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);
});