Skip to content

Commit 73db3ae

Browse files
committed
fix(NODE-6864): socket errors are not always converted to MongoNetworkErrors
1 parent 78d951b commit 73db3ae

File tree

2 files changed

+142
-3
lines changed

2 files changed

+142
-3
lines changed

src/cmap/connection.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,9 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
246246
this.lastUseTime = now();
247247

248248
this.messageStream = this.socket
249-
.on('error', this.onError.bind(this))
249+
.on('error', this.onSocketError.bind(this))
250250
.pipe(new SizedMessageTransform({ connection: this }))
251-
.on('error', this.onError.bind(this));
251+
.on('error', this.onTransformError.bind(this));
252252
this.socket.on('close', this.onClose.bind(this));
253253
this.socket.on('timeout', this.onTimeout.bind(this));
254254

@@ -303,6 +303,14 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
303303
this.lastUseTime = now();
304304
}
305305

306+
private onSocketError(cause: Error) {
307+
this.onError(new MongoNetworkError(cause.message, { cause }));
308+
}
309+
310+
private onTransformError(error: Error) {
311+
this.onError(error);
312+
}
313+
306314
public onError(error: Error) {
307315
this.cleanup(error);
308316
}
@@ -768,7 +776,6 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
768776
} finally {
769777
this.dataEvents = null;
770778
this.messageStream.pause();
771-
this.throwIfAborted();
772779
}
773780
}
774781
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { expect } from 'chai';
2+
import * as sinon from 'sinon';
3+
4+
import { ConnectionPool, type MongoClient, MongoNetworkError } from '../../mongodb';
5+
import { clearFailPoint, configureFailPoint } from '../../tools/utils';
6+
7+
describe('Socket Errors', () => {
8+
describe('when destroyed after write', () => {
9+
let client: MongoClient;
10+
let collection;
11+
12+
beforeEach(async function () {
13+
client = this.configuration.newClient({}, { appName: 'failInserts' });
14+
await client.connect();
15+
const db = client.db('closeConn');
16+
collection = db.collection('closeConn');
17+
18+
const checkOut = sinon.stub(ConnectionPool.prototype, 'checkOut').callsFake(fakeCheckout);
19+
async function fakeCheckout(...args) {
20+
const connection = await checkOut.wrappedMethod.call(this, ...args);
21+
22+
const write = sinon.stub(connection.socket, 'write').callsFake(function (...args) {
23+
queueMicrotask(() => {
24+
this.destroy(new Error('read ECONNRESET'));
25+
});
26+
return write.wrappedMethod.call(this, ...args);
27+
});
28+
29+
return connection;
30+
}
31+
});
32+
33+
afterEach(async function () {
34+
sinon.restore();
35+
await client.close();
36+
});
37+
38+
it('throws a MongoNetworkError', async () => {
39+
const error = await collection.insertOne({ name: 'test' }).catch(error => error);
40+
expect(error).to.be.instanceOf(MongoNetworkError);
41+
});
42+
});
43+
44+
describe('when destroyed after read', () => {
45+
let client: MongoClient;
46+
let collection;
47+
48+
const metadata: MongoDBMetadataUI = { requires: { mongodb: '>=4.4' } };
49+
50+
beforeEach(async function () {
51+
if (!this.configuration.filters.NodeVersionFilter.filter({ metadata })) {
52+
return;
53+
}
54+
55+
await configureFailPoint(this.configuration, {
56+
configureFailPoint: 'failCommand',
57+
mode: 'alwaysOn',
58+
data: {
59+
appName: 'failInserts',
60+
failCommands: ['insert'],
61+
blockConnection: true,
62+
blockTimeMS: 1000 // just so the server doesn't reply super fast.
63+
}
64+
});
65+
66+
client = this.configuration.newClient({}, { appName: 'failInserts' });
67+
await client.connect();
68+
const db = client.db('closeConn');
69+
collection = db.collection('closeConn');
70+
71+
const checkOut = sinon.stub(ConnectionPool.prototype, 'checkOut').callsFake(fakeCheckout);
72+
async function fakeCheckout(...args) {
73+
const connection = await checkOut.wrappedMethod.call(this, ...args);
74+
75+
const on = sinon.stub(connection.messageStream, 'on').callsFake(function (...args) {
76+
if (args[0] === 'data') {
77+
queueMicrotask(() => {
78+
connection.socket.destroy(new Error('read ECONNRESET'));
79+
});
80+
}
81+
return on.wrappedMethod.call(this, ...args);
82+
});
83+
84+
return connection;
85+
}
86+
});
87+
88+
afterEach(async function () {
89+
sinon.restore();
90+
await clearFailPoint(this.configuration);
91+
await client.close();
92+
});
93+
94+
it('throws a MongoNetworkError', metadata, async () => {
95+
const error = await collection.insertOne({ name: 'test' }).catch(error => error);
96+
expect(error).to.be.instanceOf(MongoNetworkError);
97+
});
98+
});
99+
100+
describe('when destroyed by failpoint', () => {
101+
let client: MongoClient;
102+
let collection;
103+
104+
beforeEach(async function () {
105+
await configureFailPoint(this.configuration, {
106+
configureFailPoint: 'failCommand',
107+
mode: 'alwaysOn',
108+
data: {
109+
appName: 'failInserts2',
110+
failCommands: ['insert'],
111+
closeConnection: true
112+
}
113+
});
114+
115+
client = this.configuration.newClient({}, { appName: 'failInserts2' });
116+
await client.connect();
117+
const db = client.db('closeConn');
118+
collection = db.collection('closeConn');
119+
});
120+
121+
afterEach(async function () {
122+
sinon.restore();
123+
await clearFailPoint(this.configuration);
124+
await client.close();
125+
});
126+
127+
it('throws a MongoNetworkError', async () => {
128+
const error = await collection.insertOne({ name: 'test' }).catch(error => error);
129+
expect(error, error.stack).to.be.instanceOf(MongoNetworkError);
130+
});
131+
});
132+
});

0 commit comments

Comments
 (0)