AMQP 0-9-1 library and client for Node.JS
A library for making AMQP 0-9-1 clients for Node.JS, and an AMQP 0-9-1 client for Node.JS v18+. This library does not implement AMQP1.0 or AMQP0-10.
npm install amqplib
RabbitMQ Compatibility
Only 0.10.7 and later versions of this library are compatible with RabbitMQ 4.1.0 (and later releases).
Links
Project status
- Expected to work
- Complete high-level and low-level APIs (i.e., all bits of the protocol)
- Stable APIs
- A fair few tests
- Measured test coverage
- Ports of the RabbitMQ tutorials as examples
- Used in production
Still working on:
- Getting to 100% (or very close to 100%) test coverage
Callback API example
const amqplib = require('amqplib/callback_api');
const queue = 'tasks';
amqplib.connect('amqp://localhost', (err, conn) => {
if (err) throw err;
conn.on('error', (err) => { console.error('Connection error:', err); });
conn.on('handler-error', (err, event) => { console.error(`Uncaught exception in connection ${event} listener:`, err); });
// Listener
conn.createChannel((err, ch2) => {
if (err) throw err;
ch2.on('error', (err) => { console.error('Channel error:', err); });
ch2.on('handler-error', (err, event) => { console.error(`Uncaught exception in channel ${event} listener:`, err); });
ch2.assertQueue(queue);
ch2.consume(queue, (msg) => {
if (msg !== null) {
console.log(msg.content.toString());
ch2.ack(msg);
} else {
console.log('Consumer cancelled by server');
}
});
});
// Sender
conn.createChannel((err, ch1) => {
if (err) throw err;
ch1.on('error', (err) => { console.error('Channel error:', err); });
ch1.on('handler-error', (err, event) => { console.error(`Uncaught exception in channel ${event} listener:`, err); });
ch1.assertQueue(queue);
setInterval(() => {
ch1.sendToQueue(queue, Buffer.from('something to do'));
}, 1000);
});
});
Promise/Async API example
const amqplib = require('amqplib');
(async () => {
const queue = 'tasks';
const conn = await amqplib.connect('amqp://localhost');
conn.on('error', (err) => { console.error('Connection error:', err); });
conn.on('handler-error', (err, event) => { console.error(`Uncaught exception in connection ${event} listener:`, err); });
const ch1 = await conn.createChannel();
ch1.on('error', (err) => { console.error('Channel error:', err); });
ch1.on('handler-error', (err, event) => { console.error(`Uncaught exception in channel ${event} listener:`, err); });
await ch1.assertQueue(queue);
// Listener
ch1.consume(queue, (msg) => {
if (msg !== null) {
console.log('Received:', msg.content.toString());
ch1.ack(msg);
} else {
console.log('Consumer cancelled by server');
}
});
// Sender
const ch2 = await conn.createChannel();
ch2.on('error', (err) => { console.error('Channel error:', err); });
ch2.on('handler-error', (err, event) => { console.error(`Uncaught exception in channel ${event} listener:`, err); });
setInterval(() => {
ch2.sendToQueue(queue, Buffer.from('something to do'));
}, 1000);
})();
Opt-in recovery
Automatic recovery is available as an opt-in feature through connect options:
const amqplib = require('amqplib');
const connection = await amqplib.connect('amqp://localhost', {
recovery: {
initialDelay: 200, // ms
maxDelay: 5000, // ms
factor: 2,
jitter: 0.2,
maxRetries: Infinity,
async setup(model) {
// Called after every successful (re)connect.
// Recreate topology/consumers here.
const ch = await model.createChannel();
await ch.assertQueue('tasks', {durable: true});
},
},
});
connection.on('connect', () => {
console.log('connected');
});
connection.on('disconnect', (err) => {
console.warn('disconnected', err.message);
});
Callback API supports the same option:
const amqplib = require('amqplib/callback_api');
amqplib.connect(
'amqp://localhost',
{
recovery: {
initialDelay: 200,
maxDelay: 5000,
setup(model, done) {
model.createChannel((err, ch) => {
if (err) return done(err);
ch.assertQueue('tasks', {durable: true}, done);
});
},
},
},
(err, conn) => {
if (err) throw err;
conn.on('connect', () => console.log('connected'));
},
);
Without recovery options, behavior is unchanged.
Attaching listeners before the first connection
By default connect waits for the first successful connection before
resolving (or invoking the callback), so events emitted during the initial
attempt such as connect-failed and reconnect-scheduled cannot be observed.
Set waitForConnect: false to get the connection handle immediately. You can
then attach listeners, call waitForConnect() to await the first connection,
or call close() to cancel the initial attempt. Channel operations wait for a
connection internally, so createChannel() can be called straight away.
const connection = await amqplib.connect('amqp://localhost', {
recovery: { waitForConnect: false },
});
connection.on('connect-failed', (err) => {
console.warn('connection attempt failed', err.message);
});
connection.on('reconnect-scheduled', ({ attempt, delay }) => {
console.log(`retrying (attempt ${attempt}) in ${delay}ms`);
});
await connection.waitForConnect();
The callback API returns the connection handle synchronously, so listeners can
always be attached before the first attempt. With waitForConnect: false the
callback is invoked immediately with the handle instead of after the first
connection, and waitForConnect(callback) can be used to be notified once
connected.
Custom delay strategy
By default, reconnect delays follow an exponential backoff with jitter,
controlled by initialDelay, maxDelay, factor and jitter. To use a
different strategy entirely (full jitter, decorrelated jitter, a fixed step
schedule, etc.), provide a calculateDelay function instead:
const connection = await amqplib.connect('amqp://localhost', {
recovery: {
maxRetries: Infinity,
// Called with the reconnect attempt number, starting at 1.
// Must return the delay in milliseconds.
calculateDelay(attempt) {
return Math.min(30000, 100 * 2 ** (attempt - 1));
},
},
});
maxDelay only bounds the built-in strategy. Once you provide calculateDelay,
amqplib does not cap its return value - you're responsible for enforcing your
own maximum, as the example above does with Math.min.
When calculateDelay is absent, the built-in strategy is used. If it throws,
or returns something other than a finite, non-negative number, amqplib does
not fall back to the built-in strategy. Instead recovery gives up as if
maxRetries had been exhausted: the initial connect rejects (or the
callback receives the error), pending channel operations reject, and
reconnect-failed is emitted with the error. A broken calculateDelay is a
bug in caller-supplied code, so it is surfaced rather than papered over.
Separate retry budget for the initial connection
By default maxRetries bounds every phase of recovery, including the attempts
made before the first connection succeeds. To give the very first connection
its own budget, set initialMaxRetries; once connected, maxRetries applies.
A common production posture is to fail fast at startup, so a misconfigured or
unreachable broker fails the deployment, while never giving up on a service
that has already connected:
const connection = await amqplib.connect('amqp://localhost', {
recovery: {
initialMaxRetries: 5, // give up (and reject connect) after 5 failed retries at startup
maxRetries: Infinity, // but keep reconnecting forever once connected
},
});
When the initial budget is exhausted connect rejects (or the callback receives
the error) and reconnect-failed is emitted. initialMaxRetries defaults to
maxRetries, so behaviour is unchanged unless it is set.
Error handling in event handlers
If a user-supplied event handler throws a synchronous error, the throw will propagate into amqplib internals. Depending on where in the call stack it escapes, this can silently swallow the error, or close the channel or connection.
To avoid this, register a handler-error listener on the connection and on
each channel. If a listener is present, amqplib will catch any throw from a
user event handler and deliver it there instead of letting it propagate
internally. The listener receives the thrown error and the name of the event
whose handler threw.
Note that handler-error is not a replacement for the error event.
The error event is emitted by amqplib itself when the connection or channel
encounters a protocol-level error. The handler-error event is only emitted
when your own event listener throws.
const connection = await amqp.connect('amqp://localhost');
connection.on('error', (err) => { /* handle protocol errors */ });
connection.on('handler-error', (err, event) => {
console.error(`Uncaught exception in connection ${event} listener:`, err);
});
const channel = await connection.createChannel();
channel.on('error', (err) => { /* handle protocol errors */ });
channel.on('handler-error', (err, event) => {
console.error(`Uncaught exception in channel ${event} listener:`, err);
});
If no handler-error listener is registered, behaviour is unchanged from
previous versions.
Running tests
npm test
To run the tests RabbitMQ is required. Either install it with your package manager, or use docker to run a RabbitMQ instance.
docker run -d --name amqp.test -p 5672:5672 rabbitmq
If prefer not to run RabbitMQ locally it is also possible to use a
instance of RabbitMQ hosted elsewhere. Use the URL environment
variable to configure a different amqp host to connect to. You may
also need to do this if docker is not on localhost; e.g., if it's
running in docker-machine.
One public host is dev.rabbitmq.com:
URL=amqp://dev.rabbitmq.com npm test
NB You may experience test failures due to timeouts if using the dev.rabbitmq.com instance.
You can run it under different versions of Node.JS using nave:
nave use 10 npm test
or run the tests on all supported versions of Node.JS in one go:
make test-all-nodejs
(which also needs nave installed, of course).
Lastly, setting the environment variable LOG_ERRORS will cause the
tests to output error messages encountered, to the console; this is
really only useful for checking the kind and formatting of the errors.
LOG_ERRORS=true npm test
Test coverage
make coverage
open file://`pwd`/coverage/lcov-report/index.html