amqplib 1.0.4 → 1.0.6

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,11 @@
1
1
  # Change log for amqplib
2
2
 
3
+ ## v1.0.6
4
+ - 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
+
6
+ ## v1.0.5
7
+ - Fix ConfirmChannel callbacks silently dropped on channel close when some publishes had no callback (fixes #191)
8
+
3
9
  ## v1.0.4
4
10
  - Updated build to use RabbitMQ 4.2
5
11
  - Fix memory leak in ConfirmChannel.publish when channel is already closed (fixes #842)
package/Makefile CHANGED
@@ -2,7 +2,7 @@ RABBITMQ_SRC_VERSION=v3.12.13
2
2
  JSON=amqp-rabbitmq-0.9.1.json
3
3
  AMQP_JSON=https://raw.githubusercontent.com/rabbitmq/rabbitmq-server/$(RABBITMQ_SRC_VERSION)/deps/rabbitmq_codegen/$(JSON)
4
4
 
5
- NODEJS_VERSIONS='18.1.0' '20.10.0' '22.14.0' '24.7.0'
5
+ NODEJS_VERSIONS='18.20.0' '20.10.0' '22.14.0' '24.7.0'
6
6
 
7
7
  UGLIFY=./node_modules/.bin/uglifyjs
8
8
 
@@ -1,8 +1,7 @@
1
1
  const defs = require('./defs');
2
2
  const EventEmitter = require('node:events');
3
- const BaseChannel = require('./channel').BaseChannel;
4
- const acceptMessage = require('./channel').acceptMessage;
5
3
  const Args = require('./api_args');
4
+ const { BaseChannel, acceptMessage, convertCloseFrameToError } = require('./channel');
6
5
 
7
6
  class CallbackModel extends EventEmitter {
8
7
  constructor(connection) {
@@ -179,17 +178,17 @@ class Channel extends BaseChannel {
179
178
  const fields = Args.get(queue, options);
180
179
  const cb = callbackWrapper(this, cb0);
181
180
  this.sendOrEnqueue(defs.BasicGet, fields, (err, f) => {
182
- if (err === null) {
183
- if (f.id === defs.BasicGetEmpty) {
184
- cb(null, false);
185
- } else if (f.id === defs.BasicGetOk) {
186
- this.handleMessage = acceptMessage((m) => {
187
- m.fields = f.fields;
188
- cb(null, m);
189
- });
190
- } else {
191
- cb(new Error(`Unexpected response to BasicGet: ${inspect(f)}`));
192
- }
181
+ if (err instanceof Error) return cb(err);
182
+ if (err) return cb(convertCloseFrameToError(defs.BasicGet, err));
183
+ if (f.id === defs.BasicGetEmpty) {
184
+ cb(null, false);
185
+ } else if (f.id === defs.BasicGetOk) {
186
+ this.handleMessage = acceptMessage((m) => {
187
+ m.fields = f.fields;
188
+ cb(null, m);
189
+ });
190
+ } else {
191
+ cb(new Error(`Unexpected response to BasicGet: ${inspect(f)}`));
193
192
  }
194
193
  });
195
194
  return this;
package/lib/channel.js CHANGED
@@ -33,11 +33,12 @@ class Channel extends EventEmitter {
33
33
  }),
34
34
  );
35
35
  this.on('close', function () {
36
- let cb = this.unconfirmed.shift();
37
- while (cb) {
38
- if (cb) cb(new Error('channel closed'));
39
- cb = this.unconfirmed.shift();
36
+ const err = new Error('channel closed');
37
+ for (let i = 0; i < this.unconfirmed.length; i++) {
38
+ const cb = this.unconfirmed[i];
39
+ if (cb) cb(err);
40
40
  }
41
+ this.unconfirmed = this.unconfirmed.filter((cb) => cb === false);
41
42
  });
42
43
  // message frame state machine
43
44
  this.handleMessage = acceptDeliveryOrReturn;
@@ -115,19 +116,7 @@ class Channel extends EventEmitter {
115
116
  else if (err instanceof Error) return cb(err);
116
117
  // A close frame will be given if this is the RPC awaiting reply
117
118
  // and the channel is closed by the server
118
- else {
119
- // otherwise, it's a close frame
120
- const closeReason = (err.fields.classId << 16) + err.fields.methodId;
121
- const e =
122
- method === closeReason
123
- ? fmt('Operation failed: %s; %s', methodName(method), closeMsg(err))
124
- : fmt('Channel closed by server: %s', closeMsg(err));
125
- const closeFrameError = new Error(e);
126
- closeFrameError.code = err.fields.replyCode;
127
- closeFrameError.classId = err.fields.classId;
128
- closeFrameError.methodId = err.fields.methodId;
129
- return cb(closeFrameError);
130
- }
119
+ else return cb(convertCloseFrameToError(method, err));
131
120
  }
132
121
 
133
122
  this.sendOrEnqueue(method, fields, reply);
@@ -427,6 +416,19 @@ function acceptMessage(continuation) {
427
416
  }
428
417
  }
429
418
 
419
+ function convertCloseFrameToError(method, closeFrame) {
420
+ const closeReason = (closeFrame.fields.classId << 16) + closeFrame.fields.methodId;
421
+ const e =
422
+ method === closeReason
423
+ ? fmt('Operation failed: %s; %s', methodName(method), closeMsg(closeFrame))
424
+ : fmt('Channel closed by server: %s', closeMsg(closeFrame));
425
+ const error = new Error(e);
426
+ error.code = closeFrame.fields.replyCode;
427
+ error.classId = closeFrame.fields.classId;
428
+ error.methodId = closeFrame.fields.methodId;
429
+ return error;
430
+ }
431
+
430
432
  // This adds just a bit more stuff useful for the APIs, but not
431
433
  // low-level machinery.
432
434
  class BaseChannel extends Channel {
@@ -470,3 +472,4 @@ class BaseChannel extends Channel {
470
472
  module.exports.acceptMessage = acceptMessage;
471
473
  module.exports.BaseChannel = BaseChannel;
472
474
  module.exports.Channel = Channel;
475
+ module.exports.convertCloseFrameToError = convertCloseFrameToError;
@@ -1,9 +1,8 @@
1
1
  const EventEmitter = require('node:events');
2
- const promisify = require('node:util').promisify;
2
+ const { promisify } = require('node:util');
3
3
  const defs = require('./defs');
4
- const { BaseChannel } = require('./channel');
5
- const { acceptMessage } = require('./channel');
6
4
  const Args = require('./api_args');
5
+ const { BaseChannel, acceptMessage, convertCloseFrameToError } = require('./channel');
7
6
  const { inspect } = require('./format');
8
7
 
9
8
  class ChannelModel extends EventEmitter {
@@ -159,7 +158,8 @@ class Channel extends BaseChannel {
159
158
  const fields = Args.get(queue, options);
160
159
  return new Promise((resolve, reject) => {
161
160
  this.sendOrEnqueue(defs.BasicGet, fields, (err, f) => {
162
- if (err) return reject(err);
161
+ if (err instanceof Error) return reject(err);
162
+ if (err) return reject(convertCloseFrameToError(defs.BasicGet, err));
163
163
  if (f.id === defs.BasicGetEmpty) {
164
164
  return resolve(false);
165
165
  } else if (f.id === defs.BasicGetOk) {
@@ -236,11 +236,11 @@ class ConfirmChannel extends Channel {
236
236
  waitForConfirms() {
237
237
  const awaiting = [];
238
238
  const unconfirmed = this.unconfirmed;
239
- unconfirmed.forEach((val, index) => {
240
- if (val !== null) {
239
+ unconfirmed.forEach((cb, index) => {
240
+ if (cb !== null) {
241
241
  const confirmed = new Promise((resolve, reject) => {
242
242
  unconfirmed[index] = (err) => {
243
- if (val) val(err);
243
+ if (cb) cb(err);
244
244
  if (err === null) resolve();
245
245
  else reject(err);
246
246
  };
@@ -250,10 +250,10 @@ class ConfirmChannel extends Channel {
250
250
  });
251
251
  // Channel closed
252
252
  if (!this.pending) {
253
- let cb = this.unconfirmed.shift();
254
- while (cb) {
255
- if (cb) cb(new Error('channel closed'));
256
- cb = this.unconfirmed.shift();
253
+ const err = new Error('channel closed');
254
+ while (unconfirmed.length > 0) {
255
+ const cb = unconfirmed.shift();
256
+ if (cb) cb(err);
257
257
  }
258
258
  }
259
259
  return Promise.all(awaiting);
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.4",
5
+ "version": "1.0.6",
6
6
  "description": "An AMQP 0-9-1 (e.g., RabbitMQ) library and client.",
7
7
  "repository": {
8
8
  "type": "git",
@@ -401,39 +401,18 @@ describe('Callback API', () => {
401
401
  });
402
402
  });
403
403
 
404
- /*
405
- When this test was refactored as part of the move to the node test framework
406
- it failed because only the domain error handler was ever invoked and not the
407
- channel.get error callback.
408
-
409
- The original test passed because twice.first (refactored to use util.latch) short circuits on error rather
410
- than waiting for twice.second to be invoked. I have verified that the pre-refactored
411
- amqplib does not invoke the channel.get callback when the queue does not exist.
412
- See lib/callback_model.js
413
- */
414
404
  error_test('Get from non-queue invokes error', (ch, done, dom) => {
415
405
  const decrementLatch = latch(2, () => done());
416
406
  dom.on('error', (err) => {
417
407
  assert.match(err.message, /404 \(NOT-FOUND\)/);
418
- decrementLatch(err);
408
+ decrementLatch();
419
409
  });
420
410
  ch.get('', {}, (err) => {
421
411
  assert.match(err.message, /404 \(NOT-FOUND\)/)
422
- decrementLatch(err)
412
+ decrementLatch()
423
413
  });
424
414
  });
425
415
 
426
- /*
427
- When this test was refactored as part of the move to the node test framework
428
- it failed because only the domain error handler was ever invoked and not the
429
- channel.consume error callback. Unlike the above channel.get test,
430
- channel.consume does invoke the channel.consume callback with an error, but
431
- the original test did not supply one, mistakenly providing a message handler
432
- function instead.
433
-
434
- The original test passed because twice.first (refactored to use util.latch) short circuits on error
435
- rather than waiting for twice.second to be invoked.
436
- */
437
416
  error_test('Consume from non-queue invokes error', (ch, done, dom) => {
438
417
  const decrementLatch = latch(2, done);
439
418
  dom.on('error', (err) => {
@@ -588,5 +588,25 @@ describe('Channel', () => {
588
588
  send(defs.BasicAck, { deliveryTag: 1, multiple: false }, ch);
589
589
  }, cb);
590
590
  }));
591
+
592
+ // Regression test for https://github.com/amqp-node/amqplib/issues/191
593
+ // pushConfirmCallback stores `false` for publishes with no callback.
594
+ // The close drain used `while (cb)` which stops at the first `false`,
595
+ // silently dropping all callbacks that follow it.
596
+ it('invokes all pending callbacks on close even when some publishes had no callback', channelTest((ch, cb) => {
597
+ let called = 0;
598
+ const countErr = (err) => { if (err) called++; };
599
+ ch.pushConfirmCallback(countErr); // tag = 1 — has callback
600
+ ch.pushConfirmCallback(null); // tag = 2 — no callback, stored as `false`
601
+ ch.pushConfirmCallback(countErr); // tag = 3 — has callback, dropped by while(cb) bug
602
+ open(ch).then(() => {
603
+ ch.toClosed('test close');
604
+ completes(() => {
605
+ assert.equal(2, called);
606
+ }, cb);
607
+ }, cb);
608
+ }, (_send, _wait, cb) => {
609
+ cb();
610
+ }));
591
611
  });
592
612
  });
@@ -176,6 +176,48 @@ describe('sendMessage', () => {
176
176
  });
177
177
  });
178
178
 
179
+ describe('get', () => {
180
+ it('returns false when queue is empty', () => {
181
+ return withChannel((ch) =>
182
+ ch.assertQueue('test.get-empty', QUEUE_OPTS)
183
+ .then(() => ch.purgeQueue('test.get-empty'))
184
+ .then(() => ch.get('test.get-empty', { noAck: true }))
185
+ .then((m) => assert.strictEqual(false, m))
186
+ );
187
+ });
188
+
189
+ it('returns a message when queue has messages', () => {
190
+ const msg = randomString();
191
+ return withChannel((ch) =>
192
+ ch.assertQueue('test.get-message', QUEUE_OPTS)
193
+ .then(() => ch.purgeQueue('test.get-message'))
194
+ .then(() => {
195
+ ch.sendToQueue('test.get-message', Buffer.from(msg));
196
+ return waitForMessages('test.get-message');
197
+ })
198
+ .then(() => ch.get('test.get-message', { noAck: true }))
199
+ .then((m) => {
200
+ assert(m);
201
+ assert.equal(msg, m.content.toString());
202
+ })
203
+ );
204
+ });
205
+
206
+ it('rejects with a proper error when queue does not exist', () => {
207
+ return withChannel((ch) => {
208
+ ch.once('error', ignore);
209
+ return assert.rejects(() => ch.get('', {}), (err) => {
210
+ assert(err instanceof Error);
211
+ assert.match(err.message, /NOT_FOUND/);
212
+ assert.strictEqual(404, err.code);
213
+ assert.strictEqual(60, err.classId);
214
+ assert.strictEqual(70, err.methodId);
215
+ return true;
216
+ });
217
+ });
218
+ });
219
+ });
220
+
179
221
  describe('binding, consuming', () => {
180
222
  // bind, publish, get
181
223
  it('route message', () => {