Skip to content

Commit c11bd07

Browse files
cressie176claude
andcommitted
Fix: wrap remaining connection error emits with safeEmit
Apply safeEmit to the three remaining bare this.emit('error') calls in Connection: closeWithError, onSocketError, and startHeartbeater. Adds tests for all three paths in both the without and with handler-error listener describe blocks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e727f64 commit c11bd07

2 files changed

Lines changed: 126 additions & 3 deletions

File tree

lib/connection.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ class Connection extends EventEmitter {
282282
}
283283

284284
closeWithError(reason, code, error) {
285-
this.emit('error', error);
285+
safeEmit(this, 'error', error);
286286
this.closeBecause(reason, code);
287287
}
288288

@@ -291,7 +291,7 @@ class Connection extends EventEmitter {
291291
// forestall any more calls to onSocketError, since we're signed
292292
// up for `'error'` *and* `'end'`
293293
this.expectSocketClose = true;
294-
this.emit('error', err);
294+
safeEmit(this, 'error', err);
295295
const s = stackCapture('Socket error');
296296
this.toClosed(s, err);
297297
}
@@ -364,7 +364,7 @@ class Connection extends EventEmitter {
364364
const hb = new Heart(this.heartbeat, this.checkSend.bind(this), this.checkRecv.bind(this));
365365
hb.on('timeout', () => {
366366
const hberr = new Error('Heartbeat timeout');
367-
this.emit('error', hberr);
367+
safeEmit(this, 'error', hberr);
368368
const s = stackCapture('Heartbeat timeout');
369369
this.toClosed(s, hberr);
370370
});

test/connection.test.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ describe('Connection', () => {
310310

311311
afterEach(() => {
312312
prevUncaughtExceptionListeners.forEach((h) => process.on('uncaughtException', h));
313+
heartbeat.UNITS_TO_MS = 1000;
313314
});
314315

315316
it('throw in close handler from server-initiated close is swallowed without handler-error listener', connectionTest((c, cb) => {
@@ -407,6 +408,65 @@ describe('Connection', () => {
407408
.then(() => send(defs.ConnectionUpdateSecretOk, {}, 0))
408409
.then(cb, cb);
409410
}));
411+
412+
it('throw in error handler from closeWithError becomes uncaught exception', connectionTest((c, cb) => {
413+
const expectedErr = new Error('user error handler explodes on closeWithError');
414+
process.once('uncaughtException', (err) => {
415+
assert.strictEqual(err, expectedErr);
416+
cb();
417+
});
418+
c.once('error', (err) => {
419+
assert.match(err.message, /Unexpected frame on channel 0/);
420+
throw expectedErr;
421+
});
422+
c.open(OPEN_OPTS);
423+
}, (send, wait, cb) => {
424+
handshake(send, wait)
425+
.then(() => send(defs.ChannelOpenOk, { channelId: Buffer.from('') }, 0))
426+
.then(cb, cb);
427+
}));
428+
429+
it('throw in error handler from onSocketError', connectionTest((c, cb) => {
430+
const expectedErr = new Error('user error handler explodes on socket error');
431+
process.once('uncaughtException', (err) => {
432+
assert.strictEqual(err, expectedErr);
433+
cb();
434+
});
435+
c.once('error', (err) => {
436+
assert.match(err.message, /Unexpected close/);
437+
throw expectedErr;
438+
});
439+
c.open(OPEN_OPTS, (err) => {
440+
assert.ifError(err);
441+
c.sendHeartbeat();
442+
});
443+
}, (send, wait, cb, socket) => {
444+
handshake(send, wait)
445+
.then(wait())
446+
.then(() => socket.end())
447+
.then(cb, cb);
448+
}));
449+
450+
it('throw in error handler from heartbeat timeout', connectionTest((c, cb) => {
451+
heartbeat.UNITS_TO_MS = 20;
452+
const expectedErr = new Error('user error handler explodes on heartbeat timeout');
453+
const opts = Object.create(OPEN_OPTS);
454+
opts.heartbeat = 1;
455+
process.once('uncaughtException', (err) => {
456+
assert.strictEqual(err, expectedErr);
457+
c.heartbeater.clear();
458+
cb();
459+
});
460+
c.on('error', (err) => {
461+
assert.match(err.message, /Heartbeat timeout/);
462+
throw expectedErr;
463+
});
464+
c.open(opts);
465+
}, (send, wait, cb) => {
466+
handshake(send, wait)
467+
.then(cb, cb);
468+
// conspicuously not sending anything ...
469+
}));
410470
});
411471

412472
describe('Event handler errors - with handler-error listener', () => {
@@ -419,6 +479,7 @@ describe('Connection', () => {
419479

420480
afterEach(() => {
421481
prevUncaughtExceptionListeners.forEach((h) => process.on('uncaughtException', h));
482+
heartbeat.UNITS_TO_MS = 1000;
422483
});
423484

424485
it('throw in close handler is delivered via handler-error event', connectionTest((c, cb) => {
@@ -512,6 +573,68 @@ describe('Connection', () => {
512573
.then(cb, cb);
513574
}));
514575

576+
it('throw in error handler from heartbeat timeout is delivered via handler-error event', connectionTest((c, cb) => {
577+
heartbeat.UNITS_TO_MS = 20;
578+
const expectedErr = new Error('user error handler explodes on heartbeat timeout');
579+
const opts = Object.create(OPEN_OPTS);
580+
opts.heartbeat = 1;
581+
c.on('handler-error', (err, event) => {
582+
assert.strictEqual(err, expectedErr);
583+
assert.strictEqual(event, 'error');
584+
c.heartbeater.clear();
585+
cb();
586+
});
587+
c.on('error', (err) => {
588+
assert.match(err.message, /Heartbeat timeout/);
589+
throw expectedErr;
590+
});
591+
c.open(opts);
592+
}, (send, wait, cb) => {
593+
handshake(send, wait)
594+
.then(cb, cb);
595+
// conspicuously not sending anything ...
596+
}));
597+
598+
it('throw in error handler from closeWithError is delivered via handler-error event', connectionTest((c, cb) => {
599+
const expectedErr = new Error('user error handler explodes on closeWithError');
600+
c.on('handler-error', (err, event) => {
601+
assert.strictEqual(err, expectedErr);
602+
assert.strictEqual(event, 'error');
603+
cb();
604+
});
605+
c.once('error', (err) => {
606+
assert.match(err.message, /Unexpected frame on channel 0/);
607+
throw expectedErr;
608+
});
609+
c.open(OPEN_OPTS);
610+
}, (send, wait, cb) => {
611+
handshake(send, wait)
612+
.then(() => send(defs.ChannelOpenOk, { channelId: Buffer.from('') }, 0))
613+
.then(cb, cb);
614+
}));
615+
616+
it('throw in error handler from onSocketError is delivered via handler-error event', connectionTest((c, cb) => {
617+
const expectedErr = new Error('user error handler explodes on socket error');
618+
c.on('handler-error', (err, event) => {
619+
assert.strictEqual(err, expectedErr);
620+
assert.strictEqual(event, 'error');
621+
cb();
622+
});
623+
c.once('error', (err) => {
624+
assert.match(err.message, /Unexpected close/);
625+
throw expectedErr;
626+
});
627+
c.open(OPEN_OPTS, (err) => {
628+
assert.ifError(err);
629+
c.sendHeartbeat();
630+
});
631+
}, (send, wait, cb, socket) => {
632+
handshake(send, wait)
633+
.then(wait())
634+
.then(() => socket.end())
635+
.then(cb, cb);
636+
}));
637+
515638
it('throw in handler-error handler becomes uncaught exception', connectionTest((c, cb) => {
516639
const expectedErr = new Error('handler-error handler explodes');
517640
process.once('uncaughtException', (err) => {

0 commit comments

Comments
 (0)