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 +6 -0
- package/Makefile +1 -1
- package/lib/callback_model.js +12 -13
- package/lib/channel.js +20 -17
- package/lib/channel_model.js +11 -11
- package/package.json +1 -1
- package/test/callback_api.test.js +2 -23
- package/test/channel.test.js +20 -0
- package/test/channel_api.test.js +42 -0
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.
|
|
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
|
|
package/lib/callback_model.js
CHANGED
|
@@ -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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
cb
|
|
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;
|
package/lib/channel_model.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
const EventEmitter = require('node:events');
|
|
2
|
-
const promisify = require('node:util')
|
|
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((
|
|
240
|
-
if (
|
|
239
|
+
unconfirmed.forEach((cb, index) => {
|
|
240
|
+
if (cb !== null) {
|
|
241
241
|
const confirmed = new Promise((resolve, reject) => {
|
|
242
242
|
unconfirmed[index] = (err) => {
|
|
243
|
-
if (
|
|
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
|
-
|
|
254
|
-
while (
|
|
255
|
-
|
|
256
|
-
cb
|
|
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
|
@@ -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(
|
|
408
|
+
decrementLatch();
|
|
419
409
|
});
|
|
420
410
|
ch.get('', {}, (err) => {
|
|
421
411
|
assert.match(err.message, /404 \(NOT-FOUND\)/)
|
|
422
|
-
decrementLatch(
|
|
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) => {
|
package/test/channel.test.js
CHANGED
|
@@ -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
|
});
|
package/test/channel_api.test.js
CHANGED
|
@@ -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', () => {
|