From 2088626ed20885a099ba21f925a3d0b8e0b3b3e1 Mon Sep 17 00:00:00 2001 From: Pavel Safronov Date: Mon, 27 Oct 2025 12:15:48 -0700 Subject: [PATCH 1/4] fix(NODE-7067): Wrap socket write in a try/catch to ensure errors can be properly wrapped --- src/cmap/connection.ts | 9 +++- .../convert_socket_errors.test.ts | 49 ++++++++++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index f5446ae931b..922960c3de9 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -709,7 +709,14 @@ export class Connection extends TypedEventEmitter { } } - if (this.socket.write(buffer)) return; + try { + if (this.socket.write(buffer)) return; + } catch (writeError) { + const cause = writeError as Error; + const networkError = new MongoNetworkError(cause.message, { cause }); + this.onError(networkError); + throw networkError; + } const drainEvent = once(this.socket, 'drain', options); const timeout = options?.timeoutContext?.timeoutForSocketWrite; diff --git a/test/integration/node-specific/convert_socket_errors.test.ts b/test/integration/node-specific/convert_socket_errors.test.ts index 0f3508632ea..a52f7c0efec 100644 --- a/test/integration/node-specific/convert_socket_errors.test.ts +++ b/test/integration/node-specific/convert_socket_errors.test.ts @@ -3,7 +3,9 @@ import { Duplex } from 'node:stream'; import { expect } from 'chai'; import * as sinon from 'sinon'; -import { Connection, type MongoClient, MongoNetworkError, ns } from '../../mongodb'; +import { type Collection, type Document, type MongoClient, MongoNetworkError } from '../../../src'; +import { Connection } from '../../../src/cmap/connection'; +import { ns } from '../../../src/utils'; import { clearFailPoint, configureFailPoint } from '../../tools/utils'; describe('Socket Errors', () => { @@ -39,7 +41,7 @@ describe('Socket Errors', () => { describe('when destroyed by failpoint', () => { let client: MongoClient; - let collection; + let collection: Collection; const metadata: MongoDBMetadataUI = { requires: { mongodb: '>=4.4' } }; @@ -75,4 +77,47 @@ describe('Socket Errors', () => { expect(error, error.stack).to.be.instanceOf(MongoNetworkError); }); }); + + describe('when encountering connection error', () => { + let client: MongoClient; + let collection: Collection; + + const metadata: MongoDBMetadataUI = { requires: { mongodb: '>=4.4' } }; + + beforeEach(async function () { + if (!this.configuration.filters.NodeVersionFilter.filter({ metadata })) { + return; + } + + client = this.configuration.newClient({}); + await client.connect(); + const db = client.db('closeConn'); + collection = db.collection('closeConn'); + const docs = Array.from({ length: 128 }).map((_, index) => ({ foo: index, bar: 1 })); + await collection.deleteMany({}); + await collection.insertMany(docs); + + for (const [, server] of client.topology.s.servers) { + //@ts-expect-error: private property + for (const connection of server.pool.connections) { + //@ts-expect-error: private property + const socket = connection.socket; + sinon.stub(socket, 'write').callsFake(function () { + throw new Error('This socket has been ended by the other party'); + }); + } + } + }); + + afterEach(async function () { + sinon.restore(); + await client.close(); + }); + + it('throws a MongoNetworkError and retries', metadata, async () => { + const item = await collection.findOne({}); + expect(item).to.exist; + console.log(item); + }); + }); }); From 34712e255eb40fea6c82ed7c7ec24324f74342e1 Mon Sep 17 00:00:00 2001 From: Pavel Safronov Date: Tue, 28 Oct 2025 10:21:16 -0700 Subject: [PATCH 2/4] pr feedback --- src/cmap/connection.ts | 5 ++- .../convert_socket_errors.test.ts | 40 ++++++++++++++----- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index 922960c3de9..ed7e8f6a0d5 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -712,8 +712,9 @@ export class Connection extends TypedEventEmitter { try { if (this.socket.write(buffer)) return; } catch (writeError) { - const cause = writeError as Error; - const networkError = new MongoNetworkError(cause.message, { cause }); + const networkError = new MongoNetworkError('unexpected error writing to socket', { + cause: writeError + }); this.onError(networkError); throw networkError; } diff --git a/test/integration/node-specific/convert_socket_errors.test.ts b/test/integration/node-specific/convert_socket_errors.test.ts index a52f7c0efec..9ac78fa6d0c 100644 --- a/test/integration/node-specific/convert_socket_errors.test.ts +++ b/test/integration/node-specific/convert_socket_errors.test.ts @@ -78,18 +78,13 @@ describe('Socket Errors', () => { }); }); - describe('when encountering connection error', () => { + describe('when an error is thrown writing data to a socket', () => { let client: MongoClient; let collection: Collection; - - const metadata: MongoDBMetadataUI = { requires: { mongodb: '>=4.4' } }; + let errorCount = 0; beforeEach(async function () { - if (!this.configuration.filters.NodeVersionFilter.filter({ metadata })) { - return; - } - - client = this.configuration.newClient({}); + client = this.configuration.newClient({ monitorCommands: true }); await client.connect(); const db = client.db('closeConn'); collection = db.collection('closeConn'); @@ -102,7 +97,9 @@ describe('Socket Errors', () => { for (const connection of server.pool.connections) { //@ts-expect-error: private property const socket = connection.socket; - sinon.stub(socket, 'write').callsFake(function () { + const stub = sinon.stub(socket, 'write').callsFake(function () { + errorCount++; + stub.restore(); throw new Error('This socket has been ended by the other party'); }); } @@ -114,10 +111,31 @@ describe('Socket Errors', () => { await client.close(); }); - it('throws a MongoNetworkError and retries', metadata, async () => { + it('retries and succeeds', async () => { + const initialErrorCount = errorCount; + const commandSucceededEvents: string[] = []; + const commandFailedEvents: string[] = []; + const commandStartedEvents: string[] = []; + + client.on('commandStarted', event => { + if (event.commandName === 'find') commandStartedEvents.push(event.commandName); + }); + client.on('commandSucceeded', event => { + if (event.commandName === 'find') commandSucceededEvents.push(event.commandName); + }); + client.on('commandFailed', event => { + if (event.commandName === 'find') commandFailedEvents.push(event.commandName); + }); + + // call find, fail once, succeed on retry const item = await collection.findOne({}); + // check that an object was returned expect(item).to.exist; - console.log(item); + expect(errorCount).to.be.equal(initialErrorCount + 1); + // check that we have the expected command monitoring events + expect(commandStartedEvents).to.have.length(2); + expect(commandFailedEvents).to.have.length(1); + expect(commandSucceededEvents).to.have.length(1); }); }); }); From a9b048f167d80437f58d57f72bd04df06c72a2f2 Mon Sep 17 00:00:00 2001 From: Pavel Safronov Date: Tue, 28 Oct 2025 10:47:05 -0700 Subject: [PATCH 3/4] pr feedback --- test/integration/node-specific/convert_socket_errors.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/integration/node-specific/convert_socket_errors.test.ts b/test/integration/node-specific/convert_socket_errors.test.ts index 9ac78fa6d0c..890f116c63b 100644 --- a/test/integration/node-specific/convert_socket_errors.test.ts +++ b/test/integration/node-specific/convert_socket_errors.test.ts @@ -88,9 +88,7 @@ describe('Socket Errors', () => { await client.connect(); const db = client.db('closeConn'); collection = db.collection('closeConn'); - const docs = Array.from({ length: 128 }).map((_, index) => ({ foo: index, bar: 1 })); await collection.deleteMany({}); - await collection.insertMany(docs); for (const [, server] of client.topology.s.servers) { //@ts-expect-error: private property @@ -130,7 +128,7 @@ describe('Socket Errors', () => { // call find, fail once, succeed on retry const item = await collection.findOne({}); // check that an object was returned - expect(item).to.exist; + expect(item).to.be.null; expect(errorCount).to.be.equal(initialErrorCount + 1); // check that we have the expected command monitoring events expect(commandStartedEvents).to.have.length(2); From 5952d51e47578dc9b45b2c10209bd5cbb57fcd5a Mon Sep 17 00:00:00 2001 From: Pavel Safronov Date: Tue, 28 Oct 2025 10:57:51 -0700 Subject: [PATCH 4/4] pr feedback --- .../convert_socket_errors.test.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/test/integration/node-specific/convert_socket_errors.test.ts b/test/integration/node-specific/convert_socket_errors.test.ts index 890f116c63b..6e9c38d5ae3 100644 --- a/test/integration/node-specific/convert_socket_errors.test.ts +++ b/test/integration/node-specific/convert_socket_errors.test.ts @@ -7,6 +7,7 @@ import { type Collection, type Document, type MongoClient, MongoNetworkError } f import { Connection } from '../../../src/cmap/connection'; import { ns } from '../../../src/utils'; import { clearFailPoint, configureFailPoint } from '../../tools/utils'; +import { filterForCommands } from '../shared'; describe('Socket Errors', () => { describe('when a socket emits an error', () => { @@ -81,7 +82,6 @@ describe('Socket Errors', () => { describe('when an error is thrown writing data to a socket', () => { let client: MongoClient; let collection: Collection; - let errorCount = 0; beforeEach(async function () { client = this.configuration.newClient({ monitorCommands: true }); @@ -96,7 +96,6 @@ describe('Socket Errors', () => { //@ts-expect-error: private property const socket = connection.socket; const stub = sinon.stub(socket, 'write').callsFake(function () { - errorCount++; stub.restore(); throw new Error('This socket has been ended by the other party'); }); @@ -110,26 +109,18 @@ describe('Socket Errors', () => { }); it('retries and succeeds', async () => { - const initialErrorCount = errorCount; const commandSucceededEvents: string[] = []; const commandFailedEvents: string[] = []; const commandStartedEvents: string[] = []; - client.on('commandStarted', event => { - if (event.commandName === 'find') commandStartedEvents.push(event.commandName); - }); - client.on('commandSucceeded', event => { - if (event.commandName === 'find') commandSucceededEvents.push(event.commandName); - }); - client.on('commandFailed', event => { - if (event.commandName === 'find') commandFailedEvents.push(event.commandName); - }); + client.on('commandStarted', filterForCommands('find', commandStartedEvents)); + client.on('commandSucceeded', filterForCommands('find', commandSucceededEvents)); + client.on('commandFailed', filterForCommands('find', commandFailedEvents)); // call find, fail once, succeed on retry const item = await collection.findOne({}); - // check that an object was returned + // check that we didn't find anything, as expected expect(item).to.be.null; - expect(errorCount).to.be.equal(initialErrorCount + 1); // check that we have the expected command monitoring events expect(commandStartedEvents).to.have.length(2); expect(commandFailedEvents).to.have.length(1);