Skip to content

Commit 5f6a787

Browse files
committed
fix(connect): prevent multiple callbacks in error scenarios
We treat `close`/`timeout`/`parseError`/`error` as error cases during initial handshake, but some of these do not generate errors. Additionally, in some cases multiple of these events may be emitted which leaves us in a bad state if a subsequent `error` message is emitted, since that will crash the node process. NODE-2277
1 parent ba123a6 commit 5f6a787

File tree

2 files changed

+74
-2
lines changed

2 files changed

+74
-2
lines changed

lib/core/connection/connect.js

+16-2
Original file line numberDiff line numberDiff line change
@@ -315,11 +315,25 @@ function runCommand(conn, ns, command, options, callback) {
315315
numberToReturn: 1
316316
});
317317

318+
const noop = () => {};
319+
function _callback(err, result) {
320+
callback(err, result);
321+
callback = noop;
322+
}
323+
318324
function errorHandler(err) {
319325
conn.resetSocketTimeout();
320326
CONNECTION_ERROR_EVENTS.forEach(eventName => conn.removeListener(eventName, errorHandler));
321327
conn.removeListener('message', messageHandler);
322-
callback(err, null);
328+
329+
if (err == null) {
330+
err = new MongoError(`runCommand failed for connection to '${conn.address}'`);
331+
}
332+
333+
// ignore all future errors
334+
conn.on('error', noop);
335+
336+
_callback(err, null);
323337
}
324338

325339
function messageHandler(msg) {
@@ -332,7 +346,7 @@ function runCommand(conn, ns, command, options, callback) {
332346
conn.removeListener('message', messageHandler);
333347

334348
msg.parse({ promoteValues: true });
335-
callback(null, msg.documents[0]);
349+
_callback(null, msg.documents[0]);
336350
}
337351

338352
conn.setSocketTimeout(socketTimeout);

test/core/unit/connect_tests.js

+58
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const BSON = require('bson');
44
const mock = require('mongodb-mock-server');
55
const expect = require('chai').expect;
6+
const EventEmitter = require('events');
67

78
const connect = require('../../../lib/core/connection/connect');
89
const MongoCredentials = require('../../../lib/core/auth/mongo_credentials').MongoCredentials;
@@ -98,4 +99,61 @@ describe('Connect Tests', function() {
9899
done();
99100
});
100101
});
102+
103+
describe('runCommand', function() {
104+
class MockConnection extends EventEmitter {
105+
constructor(conn) {
106+
super();
107+
this.options = { bson: new BSON() };
108+
this.conn = conn;
109+
}
110+
111+
get address() {
112+
return 'mocked';
113+
}
114+
115+
setSocketTimeout() {}
116+
resetSocketTimeout() {}
117+
destroy() {
118+
this.conn.destroy();
119+
}
120+
}
121+
122+
it('should treat non-Error generating error-like events as errors', function(done) {
123+
class ConnectionFailingWithClose extends MockConnection {
124+
write() {
125+
this.emit('close');
126+
}
127+
}
128+
129+
connect(
130+
{ host: '127.0.0.1', port: 27017, connectionType: ConnectionFailingWithClose },
131+
(err, conn) => {
132+
expect(err).to.exist;
133+
expect(err.message).to.match(/runCommand failed/);
134+
expect(conn).to.not.exist;
135+
done();
136+
}
137+
);
138+
});
139+
140+
it('should not crash the application if multiple error-like events are emitted on `runCommand`', function(done) {
141+
class ConnectionFailingWithAllEvents extends MockConnection {
142+
write() {
143+
this.emit('close');
144+
this.emit('timeout');
145+
this.emit('error');
146+
}
147+
}
148+
149+
connect(
150+
{ host: '127.0.0.1', port: 27017, connectionType: ConnectionFailingWithAllEvents },
151+
(err, conn) => {
152+
expect(err).to.exist;
153+
expect(conn).to.not.exist;
154+
done();
155+
}
156+
);
157+
});
158+
});
101159
});

0 commit comments

Comments
 (0)