amqplib 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Change log for amqplib
2
2
 
3
+ ## v1.0.7
4
+ - Fix `update-secret-ok` event not being forwarded by `ChannelModel` and `CallbackModel` (fixes #849)
5
+ - Add `handler-error` event to connections and channels. If a user-supplied event handler (e.g. `connection.on('close', ...)`, `channel.on('error', ...)`, `channel.on('delivery', ...)` etc.) throws a synchronous error, amqplib will emit a `handler-error` event on the same emitter with the thrown error and the name of the event whose handler threw — provided a `handler-error` listener is registered. If no `handler-error` listener is registered, behaviour is unchanged from previous versions. Note: in previous versions, errors thrown in connection `close` event handlers were silently swallowed; errors thrown in channel event handlers (other than `delivery`/`return`) would kill the channel and possibly the connection (fixes #334).
6
+
3
7
  ## v1.0.6
4
8
  - Fix channel.get() not invoking callback with error on channel close; previously only an error event was emitted (fixes #832). **Note:** if you use the callback API, ensure your channel.get() callbacks handle errors — they will now be invoked in error cases where previously they were not. If you use the promise API, the returned promise now rejects with a proper Error object (with .code, .classId and .methodId properties) rather than a raw close frame.
5
9
 
package/README.md CHANGED
@@ -43,10 +43,16 @@ const queue = 'tasks';
43
43
  amqplib.connect('amqp://localhost', (err, conn) => {
44
44
  if (err) throw err;
45
45
 
46
+ conn.on('error', (err) => { console.error('Connection error:', err); });
47
+ conn.on('handler-error', (err, event) => { console.error(`Uncaught exception in connection ${event} listener:`, err); });
48
+
46
49
  // Listener
47
50
  conn.createChannel((err, ch2) => {
48
51
  if (err) throw err;
49
52
 
53
+ ch2.on('error', (err) => { console.error('Channel error:', err); });
54
+ ch2.on('handler-error', (err, event) => { console.error(`Uncaught exception in channel ${event} listener:`, err); });
55
+
50
56
  ch2.assertQueue(queue);
51
57
 
52
58
  ch2.consume(queue, (msg) => {
@@ -63,6 +69,8 @@ amqplib.connect('amqp://localhost', (err, conn) => {
63
69
  conn.createChannel((err, ch1) => {
64
70
  if (err) throw err;
65
71
 
72
+ ch1.on('error', (err) => { console.error('Channel error:', err); });
73
+ ch1.on('handler-error', (err, event) => { console.error(`Uncaught exception in channel ${event} listener:`, err); });
66
74
  ch1.assertQueue(queue);
67
75
 
68
76
  setInterval(() => {
@@ -80,8 +88,12 @@ const amqplib = require('amqplib');
80
88
  (async () => {
81
89
  const queue = 'tasks';
82
90
  const conn = await amqplib.connect('amqp://localhost');
91
+ conn.on('error', (err) => { console.error('Connection error:', err); });
92
+ conn.on('handler-error', (err, event) => { console.error(`Uncaught exception in connection ${event} listener:`, err); });
83
93
 
84
94
  const ch1 = await conn.createChannel();
95
+ ch1.on('error', (err) => { console.error('Channel error:', err); });
96
+ ch1.on('handler-error', (err, event) => { console.error(`Uncaught exception in channel ${event} listener:`, err); });
85
97
  await ch1.assertQueue(queue);
86
98
 
87
99
  // Listener
@@ -96,6 +108,8 @@ const amqplib = require('amqplib');
96
108
 
97
109
  // Sender
98
110
  const ch2 = await conn.createChannel();
111
+ ch2.on('error', (err) => { console.error('Channel error:', err); });
112
+ ch2.on('handler-error', (err, event) => { console.error(`Uncaught exception in channel ${event} listener:`, err); });
99
113
 
100
114
  setInterval(() => {
101
115
  ch2.sendToQueue(queue, Buffer.from('something to do'));
@@ -104,6 +118,43 @@ const amqplib = require('amqplib');
104
118
 
105
119
  ```
106
120
 
121
+ ## Error handling in event handlers
122
+
123
+ If a user-supplied event handler throws a synchronous error, the throw will
124
+ propagate into amqplib internals. Depending on where in the call stack it
125
+ escapes, this can silently swallow the error, or close the channel or
126
+ connection.
127
+
128
+ To avoid this, register a `handler-error` listener on the connection and on
129
+ each channel. If a listener is present, amqplib will catch any throw from a
130
+ user event handler and deliver it there instead of letting it propagate
131
+ internally. The listener receives the thrown error and the name of the event
132
+ whose handler threw.
133
+
134
+ Note that `handler-error` is not a replacement for the `error` event.
135
+ The `error` event is emitted by amqplib itself when the connection or channel
136
+ encounters a protocol-level error. The `handler-error` event is only emitted
137
+ when *your own* event listener throws.
138
+
139
+ ```js
140
+ const connection = await amqp.connect('amqp://localhost');
141
+
142
+ connection.on('error', (err) => { /* handle protocol errors */ });
143
+ connection.on('handler-error', (err, event) => {
144
+ console.error(`Uncaught exception in connection ${event} listener:`, err);
145
+ });
146
+
147
+ const channel = await connection.createChannel();
148
+
149
+ channel.on('error', (err) => { /* handle protocol errors */ });
150
+ channel.on('handler-error', (err, event) => {
151
+ console.error(`Uncaught exception in channel ${event} listener:`, err);
152
+ });
153
+ ```
154
+
155
+ If no `handler-error` listener is registered, behaviour is unchanged from
156
+ previous versions.
157
+
107
158
  ## Running tests
108
159
 
109
160
  npm test
@@ -7,7 +7,7 @@ class CallbackModel extends EventEmitter {
7
7
  constructor(connection) {
8
8
  super();
9
9
  this.connection = connection;
10
- ['error', 'close', 'blocked', 'unblocked'].forEach((ev) => {
10
+ ['error', 'close', 'blocked', 'unblocked', 'update-secret-ok'].forEach((ev) => {
11
11
  connection.on(ev, this.emit.bind(this, ev));
12
12
  });
13
13
  }
package/lib/channel.js CHANGED
@@ -7,6 +7,7 @@ const EventEmitter = require('node:events');
7
7
  const fmt = require('node:util').format;
8
8
  const IllegalOperationError = require('./error').IllegalOperationError;
9
9
  const stackCapture = require('./error').stackCapture;
10
+ const safeEmit = require('./safe_emit');
10
11
 
11
12
  class Channel extends EventEmitter {
12
13
  constructor(connection) {
@@ -128,7 +129,7 @@ class Channel extends EventEmitter {
128
129
  invalidateSend(this, 'Channel closed', capturedStack);
129
130
  this.accept = invalidOp('Channel closed', capturedStack);
130
131
  this.connection.releaseChannel(this.ch);
131
- this.emit('close');
132
+ safeEmit(this, 'close');
132
133
  }
133
134
 
134
135
  // Stop being able to send and receive methods and content. Used when
@@ -188,7 +189,7 @@ class Channel extends EventEmitter {
188
189
  error.classId = defs.info(id).classId;
189
190
  error.methodId = defs.info(id).methodId;
190
191
  }
191
- this.emit('error', error);
192
+ safeEmit(this, 'error', error);
192
193
  });
193
194
  }
194
195
 
@@ -250,7 +251,7 @@ class Channel extends EventEmitter {
250
251
  }
251
252
 
252
253
  onBufferDrain() {
253
- this.emit('drain');
254
+ safeEmit(this, 'drain');
254
255
  }
255
256
 
256
257
  accept(f) {
@@ -264,12 +265,12 @@ class Channel extends EventEmitter {
264
265
 
265
266
  // confirmations, need to do confirm.select first
266
267
  case defs.BasicAck:
267
- return this.emit('ack', f.fields);
268
+ return safeEmit(this, 'ack', f.fields);
268
269
  case defs.BasicNack:
269
- return this.emit('nack', f.fields);
270
+ return safeEmit(this, 'nack', f.fields);
270
271
  case defs.BasicCancel:
271
272
  // The broker can send this if e.g., the queue is deleted.
272
- return this.emit('cancel', f.fields);
273
+ return safeEmit(this, 'cancel', f.fields);
273
274
 
274
275
  case defs.ChannelClose: {
275
276
  // Any remote closure is an error to us. Reject the pending reply
@@ -287,7 +288,7 @@ class Channel extends EventEmitter {
287
288
  error.code = f.fields.replyCode;
288
289
  error.classId = f.fields.classId;
289
290
  error.methodId = f.fields.methodId;
290
- this.emit('error', error);
291
+ safeEmit(this, 'error', error);
291
292
 
292
293
  const s = stackCapture(emsg);
293
294
  this.toClosed(s);
@@ -352,7 +353,7 @@ function acceptDeliveryOrReturn(f) {
352
353
  const fields = f.fields;
353
354
  return acceptMessage((message) => {
354
355
  message.fields = fields;
355
- this.emit(event, message);
356
+ safeEmit(this, event, message);
356
357
  });
357
358
  }
358
359
 
@@ -10,7 +10,7 @@ class ChannelModel extends EventEmitter {
10
10
  super();
11
11
  this.connection = connection;
12
12
 
13
- ['error', 'close', 'blocked', 'unblocked'].forEach((ev) => {
13
+ ['error', 'close', 'blocked', 'unblocked', 'update-secret-ok'].forEach((ev) => {
14
14
  connection.on(ev, this.emit.bind(this, ev));
15
15
  });
16
16
  }
package/lib/connection.js CHANGED
@@ -17,6 +17,7 @@ const fmt = require('node:util').format;
17
17
  const PassThrough = require('node:stream').PassThrough;
18
18
  const IllegalOperationError = require('./error').IllegalOperationError;
19
19
  const stackCapture = require('./error').stackCapture;
20
+ const safeEmit = require('./safe_emit');
20
21
 
21
22
  // High-water mark for channel write buffers, in 'objects' (which are
22
23
  // encoded frames as buffers).
@@ -281,7 +282,7 @@ class Connection extends EventEmitter {
281
282
  }
282
283
 
283
284
  closeWithError(reason, code, error) {
284
- this.emit('error', error);
285
+ safeEmit(this, 'error', error);
285
286
  this.closeBecause(reason, code);
286
287
  }
287
288
 
@@ -290,7 +291,7 @@ class Connection extends EventEmitter {
290
291
  // forestall any more calls to onSocketError, since we're signed
291
292
  // up for `'error'` *and* `'end'`
292
293
  this.expectSocketClose = true;
293
- this.emit('error', err);
294
+ safeEmit(this, 'error', err);
294
295
  const s = stackCapture('Socket error');
295
296
  this.toClosed(s, err);
296
297
  }
@@ -345,7 +346,7 @@ class Connection extends EventEmitter {
345
346
  // This is certainly true now, if it wasn't before
346
347
  this.expectSocketClose = true;
347
348
  this.stream.end();
348
- this.emit('close', maybeErr);
349
+ safeEmit(this, 'close', maybeErr);
349
350
  }
350
351
 
351
352
  _updateSecret(newSecret, reason, cb) {
@@ -363,7 +364,7 @@ class Connection extends EventEmitter {
363
364
  const hb = new Heart(this.heartbeat, this.checkSend.bind(this), this.checkRecv.bind(this));
364
365
  hb.on('timeout', () => {
365
366
  const hberr = new Error('Heartbeat timeout');
366
- this.emit('error', hberr);
367
+ safeEmit(this, 'error', hberr);
367
368
  const s = stackCapture('Heartbeat timeout');
368
369
  this.toClosed(s, hberr);
369
370
  });
@@ -585,17 +586,17 @@ function channel0(connection) {
585
586
  const e = new Error(emsg);
586
587
  e.code = f.fields.replyCode;
587
588
  if (isFatalError(e)) {
588
- connection.emit('error', e);
589
+ safeEmit(connection, 'error', e);
589
590
  }
590
591
  connection.toClosed(s, e);
591
592
  } else if (f.id === defs.ConnectionBlocked) {
592
593
  connection.blocked = true;
593
- connection.emit('blocked', f.fields.reason);
594
+ safeEmit(connection, 'blocked', f.fields.reason);
594
595
  } else if (f.id === defs.ConnectionUnblocked) {
595
596
  connection.blocked = false;
596
- connection.emit('unblocked');
597
+ safeEmit(connection, 'unblocked');
597
598
  } else if (f.id === defs.ConnectionUpdateSecretOk) {
598
- connection.emit('update-secret-ok');
599
+ safeEmit(connection, 'update-secret-ok');
599
600
  } else {
600
601
  connection.closeWithError(
601
602
  fmt('Unexpected frame on channel 0'),
@@ -0,0 +1,13 @@
1
+ function safeEmit(emitter, event, ...args) {
2
+ try {
3
+ emitter.emit(event, ...args);
4
+ } catch (e) {
5
+ if (emitter.listenerCount('handler-error') > 0) {
6
+ setImmediate(() => emitter.emit('handler-error', e, event));
7
+ } else {
8
+ throw e;
9
+ }
10
+ }
11
+ }
12
+
13
+ module.exports = safeEmit;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "amqplib",
3
3
  "homepage": "http://amqp-node.github.io/amqplib/",
4
4
  "main": "./channel_api.js",
5
- "version": "1.0.6",
5
+ "version": "1.0.7",
6
6
  "description": "An AMQP 0-9-1 (e.g., RabbitMQ) library and client.",
7
7
  "repository": {
8
8
  "type": "git",
@@ -42,6 +42,17 @@ describe('Callback API', () => {
42
42
  });
43
43
  });
44
44
  });
45
+
46
+ it('emits update-secret-ok event', (_t, done) => {
47
+ connect((err, _c) => {
48
+ assert.ifError(err);
49
+ c = _c;
50
+ c.on('update-secret-ok', () => done());
51
+ c.updateSecret(Buffer.from('new secret'), 'no reason', (err) => {
52
+ assert.ifError(err);
53
+ });
54
+ });
55
+ });
45
56
  });
46
57
 
47
58
  const channel_test_fn = (method) => {
@@ -1,4 +1,4 @@
1
- const { describe, it } = require('node:test');
1
+ const { describe, it, beforeEach, afterEach } = require('node:test');
2
2
  const assert = require('node:assert');
3
3
  const promisify = require('node:util').promisify;
4
4
  const Channel = require('../lib/channel').Channel;
@@ -609,4 +609,310 @@ describe('Channel', () => {
609
609
  cb();
610
610
  }));
611
611
  });
612
+
613
+ describe('Event handler errors - without handler-error listener', () => {
614
+ let prevUncaughtExceptionListeners;
615
+
616
+ beforeEach(() => {
617
+ prevUncaughtExceptionListeners = process.rawListeners('uncaughtException').slice();
618
+ process.removeAllListeners('uncaughtException');
619
+ });
620
+
621
+ afterEach(() => {
622
+ prevUncaughtExceptionListeners.forEach((h) => process.on('uncaughtException', h));
623
+ });
624
+
625
+ it('throw in close handler kills the connection without handler-error listener', channelTest((ch, cb, conn) => {
626
+ const expectedErr = new Error('user close handler explodes');
627
+ let connErrorSeen = false;
628
+ // The throw propagates to acceptLoop → frameError → onSocketError → connection.emit('error')
629
+ conn.on('error', (err) => {
630
+ assert.strictEqual(err, expectedErr);
631
+ connErrorSeen = true;
632
+ });
633
+ // The ChannelCloseOk buffered before the kill is flushed by the mux after the
634
+ // stream is ended, producing ERR_STREAM_PUSH_AFTER_EOF. Absorb it and use it
635
+ // as the signal to end the test, since it fires after the conn error.
636
+ process.once('uncaughtException', (err) => {
637
+ assert.strictEqual(err.code, 'ERR_STREAM_PUSH_AFTER_EOF');
638
+ assert.ok(connErrorSeen);
639
+ cb();
640
+ });
641
+ ch.once('error', (err) => { assert.match(err.message, /Channel closed by server/); });
642
+ ch.on('close', () => { throw expectedErr; });
643
+ open(ch);
644
+ }, (send, _wait, cb, ch) => {
645
+ completes(() => {
646
+ send(defs.ChannelClose, {
647
+ replyText: 'Forced close',
648
+ replyCode: defs.constants.CHANNEL_ERROR,
649
+ classId: 0,
650
+ methodId: 0,
651
+ }, ch);
652
+ }, cb);
653
+ }));
654
+
655
+ it('throw in error handler becomes uncaught exception', channelTest((ch, cb) => {
656
+ const expectedErr = new Error('user error handler explodes');
657
+ process.once('uncaughtException', (err) => {
658
+ assert.strictEqual(err, expectedErr);
659
+ cb();
660
+ });
661
+ ch.on('error', () => { throw expectedErr; });
662
+ open(ch);
663
+ }, (send, wait, cb, ch) => {
664
+ send(defs.ChannelClose, {
665
+ replyText: 'Forced close',
666
+ replyCode: defs.constants.CHANNEL_ERROR,
667
+ classId: 0,
668
+ methodId: 0,
669
+ }, ch);
670
+ wait(defs.ChannelCloseOk)().then(cb, cb);
671
+ }));
672
+
673
+ it('throw in drain handler becomes uncaught exception', channelTest((ch, cb) => {
674
+ const expectedErr = new Error('user drain handler explodes');
675
+ process.once('uncaughtException', (err) => {
676
+ assert.strictEqual(err, expectedErr);
677
+ cb();
678
+ });
679
+ ch.on('drain', () => { throw expectedErr; });
680
+ // Call outside a Promise chain so the throw becomes an uncaughtException
681
+ open(ch).then(() => setImmediate(() => ch.onBufferDrain()));
682
+ }, (_send, _wait, cb) => { cb(); }));
683
+
684
+ it('throw in ack handler becomes uncaught exception', channelTest((ch, cb) => {
685
+ const expectedErr = new Error('user ack handler explodes');
686
+ process.once('uncaughtException', (err) => {
687
+ assert.strictEqual(err, expectedErr);
688
+ cb();
689
+ });
690
+ ch.on('ack', () => { throw expectedErr; });
691
+ open(ch);
692
+ }, (send, _wait, cb, ch) => {
693
+ completes(() => {
694
+ send(defs.BasicAck, { deliveryTag: 1, multiple: false }, ch);
695
+ }, cb);
696
+ }));
697
+
698
+ it('throw in nack handler becomes uncaught exception', channelTest((ch, cb) => {
699
+ const expectedErr = new Error('user nack handler explodes');
700
+ process.once('uncaughtException', (err) => {
701
+ assert.strictEqual(err, expectedErr);
702
+ cb();
703
+ });
704
+ ch.on('nack', () => { throw expectedErr; });
705
+ open(ch);
706
+ }, (send, _wait, cb, ch) => {
707
+ completes(() => {
708
+ send(defs.BasicNack, { deliveryTag: 1, multiple: false, requeue: false }, ch);
709
+ }, cb);
710
+ }));
711
+
712
+ it('throw in cancel handler becomes uncaught exception', channelTest((ch, cb) => {
713
+ const expectedErr = new Error('user cancel handler explodes');
714
+ process.once('uncaughtException', (err) => {
715
+ assert.strictEqual(err, expectedErr);
716
+ cb();
717
+ });
718
+ ch.on('cancel', () => { throw expectedErr; });
719
+ open(ch);
720
+ }, (send, _wait, cb, ch) => {
721
+ completes(() => {
722
+ send(defs.BasicCancel, { consumerTag: 'tag', nowait: false }, ch);
723
+ }, cb);
724
+ }));
725
+
726
+ it('throw in delivery handler closes channel with error', channelTest((ch, cb) => {
727
+ // acceptMessageFrame catches the throw and routes it to closeWithError,
728
+ // which fires channel 'error' then 'close' — it does NOT become an uncaughtException.
729
+ const expectedErr = new Error('user delivery handler explodes');
730
+ const decrementLatch = latch(2, cb);
731
+ ch.on('error', (err) => {
732
+ assert.strictEqual(err, expectedErr);
733
+ decrementLatch();
734
+ });
735
+ ch.on('close', decrementLatch);
736
+ ch.on('delivery', () => { throw expectedErr; });
737
+ open(ch);
738
+ }, (send, wait, cb, ch) => {
739
+ send(defs.BasicDeliver, DELIVER_FIELDS, ch, Buffer.from('hello'));
740
+ wait(defs.ChannelClose)()
741
+ .then(() => send(defs.ChannelCloseOk, {}, ch))
742
+ .then(cb, cb);
743
+ }));
744
+
745
+ it('throw in return handler closes channel with error', channelTest((ch, cb) => {
746
+ // acceptMessageFrame catches the throw and routes it to closeWithError.
747
+ const expectedErr = new Error('user return handler explodes');
748
+ const decrementLatch = latch(2, cb);
749
+ ch.on('error', (err) => {
750
+ assert.strictEqual(err, expectedErr);
751
+ decrementLatch();
752
+ });
753
+ ch.on('close', decrementLatch);
754
+ ch.on('return', () => { throw expectedErr; });
755
+ open(ch);
756
+ }, (send, wait, cb, ch) => {
757
+ send(defs.BasicReturn, DELIVER_FIELDS, ch, Buffer.from('hello'));
758
+ wait(defs.ChannelClose)()
759
+ .then(() => send(defs.ChannelCloseOk, {}, ch))
760
+ .then(cb, cb);
761
+ }));
762
+ });
763
+
764
+ describe('Event handler errors - with handler-error listener', () => {
765
+ let prevUncaughtExceptionListeners;
766
+
767
+ beforeEach(() => {
768
+ prevUncaughtExceptionListeners = process.rawListeners('uncaughtException').slice();
769
+ process.removeAllListeners('uncaughtException');
770
+ });
771
+
772
+ afterEach(() => {
773
+ prevUncaughtExceptionListeners.forEach((h) => process.on('uncaughtException', h));
774
+ });
775
+
776
+ it('throw in close handler is delivered via handler-error event', channelTest((ch, cb) => {
777
+ const expectedErr = new Error('user close handler explodes');
778
+ ch.on('handler-error', (err, event) => {
779
+ assert.strictEqual(err, expectedErr);
780
+ assert.strictEqual(event, 'close');
781
+ cb();
782
+ });
783
+ // A server-initiated ChannelClose always fires 'error' before 'close' (line 291 in channel.js).
784
+ // Handle it so only the throw from the close handler reaches handler-error.
785
+ ch.once('error', (err) => { assert.match(err.message, /Channel closed by server/); });
786
+ ch.on('close', () => { throw expectedErr; });
787
+ open(ch);
788
+ }, (send, wait, cb, ch) => {
789
+ send(defs.ChannelClose, {
790
+ replyText: 'Forced close',
791
+ replyCode: defs.constants.CHANNEL_ERROR,
792
+ classId: 0,
793
+ methodId: 0,
794
+ }, ch);
795
+ wait(defs.ChannelCloseOk)().then(cb, cb);
796
+ }));
797
+
798
+ it('throw in error handler is delivered via handler-error event', channelTest((ch, cb) => {
799
+ const expectedErr = new Error('user error handler explodes');
800
+ ch.on('handler-error', (err, event) => {
801
+ assert.strictEqual(err, expectedErr);
802
+ assert.strictEqual(event, 'error');
803
+ cb();
804
+ });
805
+ ch.on('error', () => { throw expectedErr; });
806
+ open(ch);
807
+ }, (send, wait, cb, ch) => {
808
+ send(defs.ChannelClose, {
809
+ replyText: 'Forced close',
810
+ replyCode: defs.constants.CHANNEL_ERROR,
811
+ classId: 0,
812
+ methodId: 0,
813
+ }, ch);
814
+ wait(defs.ChannelCloseOk)().then(cb, cb);
815
+ }));
816
+
817
+ it('throw in drain handler is delivered via handler-error event', channelTest((ch, cb) => {
818
+ const expectedErr = new Error('user drain handler explodes');
819
+ ch.on('handler-error', (err, event) => {
820
+ assert.strictEqual(err, expectedErr);
821
+ assert.strictEqual(event, 'drain');
822
+ cb();
823
+ });
824
+ ch.on('drain', () => { throw expectedErr; });
825
+ open(ch).then(() => setImmediate(() => ch.onBufferDrain()));
826
+ }, (_send, _wait, cb) => { cb(); }));
827
+
828
+ it('throw in ack handler is delivered via handler-error event', channelTest((ch, cb) => {
829
+ const expectedErr = new Error('user ack handler explodes');
830
+ ch.on('handler-error', (err, event) => {
831
+ assert.strictEqual(err, expectedErr);
832
+ assert.strictEqual(event, 'ack');
833
+ cb();
834
+ });
835
+ ch.on('ack', () => { throw expectedErr; });
836
+ open(ch);
837
+ }, (send, _wait, cb, ch) => {
838
+ completes(() => {
839
+ send(defs.BasicAck, { deliveryTag: 1, multiple: false }, ch);
840
+ }, cb);
841
+ }));
842
+
843
+ it('throw in nack handler is delivered via handler-error event', channelTest((ch, cb) => {
844
+ const expectedErr = new Error('user nack handler explodes');
845
+ ch.on('handler-error', (err, event) => {
846
+ assert.strictEqual(err, expectedErr);
847
+ assert.strictEqual(event, 'nack');
848
+ cb();
849
+ });
850
+ ch.on('nack', () => { throw expectedErr; });
851
+ open(ch);
852
+ }, (send, _wait, cb, ch) => {
853
+ completes(() => {
854
+ send(defs.BasicNack, { deliveryTag: 1, multiple: false, requeue: false }, ch);
855
+ }, cb);
856
+ }));
857
+
858
+ it('throw in cancel handler is delivered via handler-error event', channelTest((ch, cb) => {
859
+ const expectedErr = new Error('user cancel handler explodes');
860
+ ch.on('handler-error', (err, event) => {
861
+ assert.strictEqual(err, expectedErr);
862
+ assert.strictEqual(event, 'cancel');
863
+ cb();
864
+ });
865
+ ch.on('cancel', () => { throw expectedErr; });
866
+ open(ch);
867
+ }, (send, _wait, cb, ch) => {
868
+ completes(() => {
869
+ send(defs.BasicCancel, { consumerTag: 'tag', nowait: false }, ch);
870
+ }, cb);
871
+ }));
872
+
873
+ it('throw in delivery handler is delivered via handler-error event', channelTest((ch, cb) => {
874
+ const expectedErr = new Error('user delivery handler explodes');
875
+ ch.on('handler-error', (err, event) => {
876
+ assert.strictEqual(err, expectedErr);
877
+ assert.strictEqual(event, 'delivery');
878
+ cb();
879
+ });
880
+ ch.on('delivery', () => { throw expectedErr; });
881
+ open(ch);
882
+ }, (send, _wait, cb, ch) => {
883
+ completes(() => {
884
+ send(defs.BasicDeliver, DELIVER_FIELDS, ch, Buffer.from('hello'));
885
+ }, cb);
886
+ }));
887
+
888
+ it('throw in return handler is delivered via handler-error event', channelTest((ch, cb) => {
889
+ const expectedErr = new Error('user return handler explodes');
890
+ ch.on('handler-error', (err, event) => {
891
+ assert.strictEqual(err, expectedErr);
892
+ assert.strictEqual(event, 'return');
893
+ cb();
894
+ });
895
+ ch.on('return', () => { throw expectedErr; });
896
+ open(ch);
897
+ }, (send, _wait, cb, ch) => {
898
+ completes(() => {
899
+ send(defs.BasicReturn, DELIVER_FIELDS, ch, Buffer.from('hello'));
900
+ }, cb);
901
+ }));
902
+
903
+ it('throw in handler-error handler becomes uncaught exception', channelTest((ch, cb) => {
904
+ const expectedErr = new Error('handler-error handler explodes');
905
+ process.once('uncaughtException', (err) => {
906
+ assert.strictEqual(err, expectedErr);
907
+ cb();
908
+ });
909
+ ch.on('handler-error', () => { throw expectedErr; });
910
+ ch.on('ack', () => { throw new Error('user ack handler explodes'); });
911
+ open(ch);
912
+ }, (send, _wait, cb, ch) => {
913
+ completes(() => {
914
+ send(defs.BasicAck, { deliveryTag: 1, multiple: false }, ch);
915
+ }, cb);
916
+ }));
917
+ });
612
918
  });
@@ -25,6 +25,15 @@ describe('updateSecret', () => {
25
25
  .finally(() => c.close())
26
26
  );
27
27
  });
28
+
29
+ it('emits update-secret-ok event', () => {
30
+ return connect().then((c) =>
31
+ new Promise((resolve, reject) => {
32
+ c.on('update-secret-ok', resolve);
33
+ c.updateSecret(Buffer.from('new secret'), 'no reason').catch(reject);
34
+ }).finally(() => c.close())
35
+ );
36
+ });
28
37
  });
29
38
 
30
39
  describe('assert, check, delete', () => {
@@ -300,6 +300,357 @@ describe('Connection', () => {
300
300
  }));
301
301
  });
302
302
 
303
+ describe('Event handler errors - without handler-event listener', () => {
304
+ let prevUncaughtExceptionListeners;
305
+
306
+ beforeEach(() => {
307
+ prevUncaughtExceptionListeners = process.rawListeners('uncaughtException').slice();
308
+ process.removeAllListeners('uncaughtException');
309
+ });
310
+
311
+ afterEach(() => {
312
+ prevUncaughtExceptionListeners.forEach((h) => process.on('uncaughtException', h));
313
+ heartbeat.UNITS_TO_MS = 1000;
314
+ });
315
+
316
+ it('throw in close handler from server-initiated close is swallowed without handler-error listener', connectionTest((c, cb) => {
317
+ c.on('close', () => { throw new Error('user handler explodes'); });
318
+ c.open(OPEN_OPTS);
319
+ cb();
320
+ }, (send, wait, cb) => {
321
+ handshake(send, wait)
322
+ .then(() => send(defs.ConnectionClose, {
323
+ replyText: 'Begone',
324
+ replyCode: defs.constants.CONNECTION_FORCED,
325
+ methodId: 0,
326
+ classId: 0,
327
+ }))
328
+ .then(wait(defs.ConnectionCloseOk))
329
+ .then(cb, cb);
330
+ }));
331
+
332
+ it('throw in close handler from client-initiated close is swallowed without handler-error listener', connectionTest((c, cb) => {
333
+ c.open(OPEN_OPTS, (err) => {
334
+ assert.ifError(err);
335
+ c.on('close', () => { throw new Error('user handler explodes on client close'); });
336
+ c.close();
337
+ cb();
338
+ });
339
+ }, (send, wait, cb) => {
340
+ handshake(send, wait)
341
+ .then(wait(defs.ConnectionClose))
342
+ .then(() => send(defs.ConnectionCloseOk, {}))
343
+ .then(cb, cb);
344
+ }));
345
+
346
+ it('throw in error handler becomes uncaught exception', connectionTest((c, cb) => {
347
+ const expectedErr = new Error('user error handler explodes');
348
+ process.once('uncaughtException', (err) => {
349
+ assert.strictEqual(err, expectedErr);
350
+ cb();
351
+ });
352
+ c.on('error', () => { throw expectedErr; });
353
+ c.open(OPEN_OPTS);
354
+ }, (send, wait, cb) => {
355
+ handshake(send, wait)
356
+ .then(() => send(defs.ConnectionClose, {
357
+ replyText: 'Begone',
358
+ replyCode: defs.constants.INTERNAL_ERROR,
359
+ methodId: 0,
360
+ classId: 0,
361
+ }))
362
+ .then(wait(defs.ConnectionCloseOk))
363
+ .then(cb, cb);
364
+ }));
365
+
366
+ it('throw in blocked handler becomes uncaught exception', connectionTest((c, cb) => {
367
+ const expectedErr = new Error('user blocked handler explodes');
368
+ process.once('uncaughtException', (err) => {
369
+ assert.strictEqual(err, expectedErr);
370
+ cb();
371
+ });
372
+ c.on('blocked', () => { throw expectedErr; });
373
+ c.open(OPEN_OPTS);
374
+ }, (send, wait, cb) => {
375
+ handshake(send, wait)
376
+ .then(() => send(defs.ConnectionBlocked, { reason: 'memory' }, 0))
377
+ .then(cb, cb);
378
+ }));
379
+
380
+ it('throw in unblocked handler becomes uncaught exception', connectionTest((c, cb) => {
381
+ const expectedErr = new Error('user unblocked handler explodes');
382
+ process.once('uncaughtException', (err) => {
383
+ assert.strictEqual(err, expectedErr);
384
+ cb();
385
+ });
386
+ c.on('unblocked', () => { throw expectedErr; });
387
+ c.open(OPEN_OPTS);
388
+ }, (send, wait, cb) => {
389
+ handshake(send, wait)
390
+ .then(() => send(defs.ConnectionUnblocked, {}, 0))
391
+ .then(cb, cb);
392
+ }));
393
+
394
+ it('throw in update-secret-ok handler becomes uncaught exception', connectionTest((c, cb) => {
395
+ const expectedErr = new Error('user update-secret-ok handler explodes');
396
+ process.once('uncaughtException', (err) => {
397
+ assert.strictEqual(err, expectedErr);
398
+ cb();
399
+ });
400
+ c.open(OPEN_OPTS, (err) => {
401
+ assert.ifError(err);
402
+ c.on('update-secret-ok', () => { throw expectedErr; });
403
+ c._updateSecret(Buffer.from('new secret'), 'reason', () => {});
404
+ });
405
+ }, (send, wait, cb) => {
406
+ handshake(send, wait)
407
+ .then(wait(defs.ConnectionUpdateSecret))
408
+ .then(() => send(defs.ConnectionUpdateSecretOk, {}, 0))
409
+ .then(cb, cb);
410
+ }));
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
+ }));
470
+ });
471
+
472
+ describe('Event handler errors - with handler-error listener', () => {
473
+ let prevUncaughtExceptionListeners;
474
+
475
+ beforeEach(() => {
476
+ prevUncaughtExceptionListeners = process.rawListeners('uncaughtException').slice();
477
+ process.removeAllListeners('uncaughtException');
478
+ });
479
+
480
+ afterEach(() => {
481
+ prevUncaughtExceptionListeners.forEach((h) => process.on('uncaughtException', h));
482
+ heartbeat.UNITS_TO_MS = 1000;
483
+ });
484
+
485
+ it('throw in close handler is delivered via handler-error event', connectionTest((c, cb) => {
486
+ const expectedErr = new Error('user close handler explodes');
487
+ c.on('handler-error', (err, event) => {
488
+ assert.strictEqual(err, expectedErr);
489
+ assert.strictEqual(event, 'close');
490
+ cb();
491
+ });
492
+ c.on('close', () => { throw expectedErr; });
493
+ c.open(OPEN_OPTS);
494
+ }, (send, wait, cb) => {
495
+ handshake(send, wait)
496
+ .then(() => send(defs.ConnectionClose, {
497
+ replyText: 'Begone',
498
+ replyCode: defs.constants.CONNECTION_FORCED,
499
+ methodId: 0,
500
+ classId: 0,
501
+ }))
502
+ .then(wait(defs.ConnectionCloseOk))
503
+ .then(cb, cb);
504
+ }));
505
+
506
+ it('throw in error handler is delivered via handler-error event', connectionTest((c, cb) => {
507
+ const expectedErr = new Error('user error handler explodes');
508
+ c.on('handler-error', (err, event) => {
509
+ assert.strictEqual(err, expectedErr);
510
+ assert.strictEqual(event, 'error');
511
+ cb();
512
+ });
513
+ c.on('error', () => { throw expectedErr; });
514
+ c.open(OPEN_OPTS);
515
+ }, (send, wait, cb) => {
516
+ handshake(send, wait)
517
+ .then(() => send(defs.ConnectionClose, {
518
+ replyText: 'Begone',
519
+ replyCode: defs.constants.INTERNAL_ERROR,
520
+ methodId: 0,
521
+ classId: 0,
522
+ }))
523
+ .then(wait(defs.ConnectionCloseOk))
524
+ .then(cb, cb);
525
+ }));
526
+
527
+ it('throw in blocked handler is delivered via handler-error event', connectionTest((c, cb) => {
528
+ const expectedErr = new Error('user blocked handler explodes');
529
+ c.on('handler-error', (err, event) => {
530
+ assert.strictEqual(err, expectedErr);
531
+ assert.strictEqual(event, 'blocked');
532
+ cb();
533
+ });
534
+ c.on('blocked', () => { throw expectedErr; });
535
+ c.open(OPEN_OPTS);
536
+ }, (send, wait, cb) => {
537
+ handshake(send, wait)
538
+ .then(() => send(defs.ConnectionBlocked, { reason: 'memory' }, 0))
539
+ .then(cb, cb);
540
+ }));
541
+
542
+ it('throw in unblocked handler is delivered via handler-error event', connectionTest((c, cb) => {
543
+ const expectedErr = new Error('user unblocked handler explodes');
544
+ c.on('handler-error', (err, event) => {
545
+ assert.strictEqual(err, expectedErr);
546
+ assert.strictEqual(event, 'unblocked');
547
+ cb();
548
+ });
549
+ c.on('unblocked', () => { throw expectedErr; });
550
+ c.open(OPEN_OPTS);
551
+ }, (send, wait, cb) => {
552
+ handshake(send, wait)
553
+ .then(() => send(defs.ConnectionUnblocked, {}, 0))
554
+ .then(cb, cb);
555
+ }));
556
+
557
+ it('throw in update-secret-ok handler is delivered via handler-error event', connectionTest((c, cb) => {
558
+ const expectedErr = new Error('user update-secret-ok handler explodes');
559
+ c.on('handler-error', (err, event) => {
560
+ assert.strictEqual(err, expectedErr);
561
+ assert.strictEqual(event, 'update-secret-ok');
562
+ cb();
563
+ });
564
+ c.open(OPEN_OPTS, (err) => {
565
+ assert.ifError(err);
566
+ c.on('update-secret-ok', () => { throw expectedErr; });
567
+ c._updateSecret(Buffer.from('new secret'), 'reason', () => {});
568
+ });
569
+ }, (send, wait, cb) => {
570
+ handshake(send, wait)
571
+ .then(wait(defs.ConnectionUpdateSecret))
572
+ .then(() => send(defs.ConnectionUpdateSecretOk, {}, 0))
573
+ .then(cb, cb);
574
+ }));
575
+
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
+
638
+ it('throw in handler-error handler becomes uncaught exception', connectionTest((c, cb) => {
639
+ const expectedErr = new Error('handler-error handler explodes');
640
+ process.once('uncaughtException', (err) => {
641
+ assert.strictEqual(err, expectedErr);
642
+ cb();
643
+ });
644
+ c.on('handler-error', () => { throw expectedErr; });
645
+ c.on('blocked', () => { throw new Error('user blocked handler explodes'); });
646
+ c.open(OPEN_OPTS);
647
+ }, (send, wait, cb) => {
648
+ handshake(send, wait)
649
+ .then(() => send(defs.ConnectionBlocked, { reason: 'memory' }, 0))
650
+ .then(cb, cb);
651
+ }));
652
+ });
653
+
303
654
  describe('heartbeats', () => {
304
655
 
305
656
  beforeEach(() => {