Skip to content

Commit 66c18b7

Browse files
fix(NODE-7067): Wrap socket write in a try/catch to ensure errors can be properly wrapped (#4759)
1 parent 52791bc commit 66c18b7

File tree

2 files changed

+61
-3
lines changed

2 files changed

+61
-3
lines changed

src/cmap/connection.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,15 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
709709
}
710710
}
711711

712-
if (this.socket.write(buffer)) return;
712+
try {
713+
if (this.socket.write(buffer)) return;
714+
} catch (writeError) {
715+
const networkError = new MongoNetworkError('unexpected error writing to socket', {
716+
cause: writeError
717+
});
718+
this.onError(networkError);
719+
throw networkError;
720+
}
713721

714722
const drainEvent = once<void>(this.socket, 'drain', options);
715723
const timeout = options?.timeoutContext?.timeoutForSocketWrite;

test/integration/node-specific/convert_socket_errors.test.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import { Duplex } from 'node:stream';
33
import { expect } from 'chai';
44
import * as sinon from 'sinon';
55

6-
import { type MongoClient, MongoNetworkError } from '../../../src';
6+
import { type Collection, type Document, type MongoClient, MongoNetworkError } from '../../../src';
77
import { Connection } from '../../../src/cmap/connection';
88
import { ns } from '../../../src/utils';
99
import { clearFailPoint, configureFailPoint } from '../../tools/utils';
10+
import { filterForCommands } from '../shared';
1011

1112
describe('Socket Errors', () => {
1213
describe('when a socket emits an error', () => {
@@ -41,7 +42,7 @@ describe('Socket Errors', () => {
4142

4243
describe('when destroyed by failpoint', () => {
4344
let client: MongoClient;
44-
let collection;
45+
let collection: Collection<Document>;
4546

4647
const metadata: MongoDBMetadataUI = { requires: { mongodb: '>=4.4' } };
4748

@@ -77,4 +78,53 @@ describe('Socket Errors', () => {
7778
expect(error, error.stack).to.be.instanceOf(MongoNetworkError);
7879
});
7980
});
81+
82+
describe('when an error is thrown writing data to a socket', () => {
83+
let client: MongoClient;
84+
let collection: Collection<Document>;
85+
86+
beforeEach(async function () {
87+
client = this.configuration.newClient({ monitorCommands: true });
88+
await client.connect();
89+
const db = client.db('closeConn');
90+
collection = db.collection('closeConn');
91+
await collection.deleteMany({});
92+
93+
for (const [, server] of client.topology.s.servers) {
94+
//@ts-expect-error: private property
95+
for (const connection of server.pool.connections) {
96+
//@ts-expect-error: private property
97+
const socket = connection.socket;
98+
const stub = sinon.stub(socket, 'write').callsFake(function () {
99+
stub.restore();
100+
throw new Error('This socket has been ended by the other party');
101+
});
102+
}
103+
}
104+
});
105+
106+
afterEach(async function () {
107+
sinon.restore();
108+
await client.close();
109+
});
110+
111+
it('retries and succeeds', async () => {
112+
const commandSucceededEvents: string[] = [];
113+
const commandFailedEvents: string[] = [];
114+
const commandStartedEvents: string[] = [];
115+
116+
client.on('commandStarted', filterForCommands('find', commandStartedEvents));
117+
client.on('commandSucceeded', filterForCommands('find', commandSucceededEvents));
118+
client.on('commandFailed', filterForCommands('find', commandFailedEvents));
119+
120+
// call find, fail once, succeed on retry
121+
const item = await collection.findOne({});
122+
// check that we didn't find anything, as expected
123+
expect(item).to.be.null;
124+
// check that we have the expected command monitoring events
125+
expect(commandStartedEvents).to.have.length(2);
126+
expect(commandFailedEvents).to.have.length(1);
127+
expect(commandSucceededEvents).to.have.length(1);
128+
});
129+
});
80130
});

0 commit comments

Comments
 (0)