From 94a37d02c69934052e8a61632dbb33076c5ff2f9 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 7 Oct 2025 12:12:01 -0400 Subject: [PATCH 1/7] feat(NODE-7223): run checkout on connect regardless of credentials --- src/sdam/topology.ts | 2 +- .../node-specific/mongo_client.test.ts | 29 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/sdam/topology.ts b/src/sdam/topology.ts index 3d088caf6c3..431d5abbb53 100644 --- a/src/sdam/topology.ts +++ b/src/sdam/topology.ts @@ -470,7 +470,7 @@ export class Topology extends TypedEventEmitter { ); const skipPingOnConnect = this.s.options.__skipPingOnConnect === true; - if (!skipPingOnConnect && this.s.credentials) { + if (!skipPingOnConnect) { const connection = await server.pool.checkOut({ timeoutContext: timeoutContext }); server.pool.checkIn(connection); stateTransition(this, STATE_CONNECTED); diff --git a/test/integration/node-specific/mongo_client.test.ts b/test/integration/node-specific/mongo_client.test.ts index 9e0394013cd..05120dd3eef 100644 --- a/test/integration/node-specific/mongo_client.test.ts +++ b/test/integration/node-specific/mongo_client.test.ts @@ -12,6 +12,7 @@ import { Db, getTopology, MongoClient, + MongoNetworkError, MongoNotConnectedError, MongoServerSelectionError, ReadPreference, @@ -333,6 +334,28 @@ describe('class MongoClient', function () { } }); }); + + it('throws ENOTFOUND error when connecting to non-existent host with no auth and loadBalanced=true', async function () { + const configuration = this.configuration; + const client = configuration.newClient( + 'mongodb://iLoveJavaScript:27017/test?loadBalanced=true', + { serverSelectionTimeoutMS: 100 } + ); + + const error = await client.connect().catch(error => error); + expect(error).to.be.instanceOf(MongoNetworkError); // not server selection like other topologies + expect(error.message).to.match(/ENOTFOUND/); + }); + + it('throws an error when srv is not a real record', async function () { + const client = this.configuration.newClient('mongodb+srv://iLoveJavaScript/test', { + serverSelectionTimeoutMS: 100 + }); + + const error = await client.connect().catch(error => error); + expect(error).to.be.instanceOf(Error); + expect(error.message).to.match(/ENOTFOUND/); + }); }); it('Should correctly pass through appname', { @@ -600,15 +623,17 @@ describe('class MongoClient', function () { ); it( - 'does not checkout connection when authentication is disabled', + 'checks out connection to confirm connectivity even when authentication is disabled', { requires: { auth: 'disabled' } }, async function () { const checkoutStartedEvents = []; client.on('connectionCheckOutStarted', event => { checkoutStartedEvents.push(event); }); + const checkoutStarted = once(client, 'connectionCheckOutStarted'); await client.connect(); - expect(checkoutStartedEvents).to.be.empty; + const checkout = await checkoutStarted; + expect(checkout).to.exist; expect(client).to.have.property('topology').that.is.instanceOf(Topology); } ); From 92c379a60260d151ca398a23f57466e5e0edd7ee Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 7 Oct 2025 12:28:28 -0400 Subject: [PATCH 2/7] fix unit --- ...rver_discovery_and_monitoring.spec.test.ts | 9 ++++- .../server_discovery_and_monitoring.test.ts | 9 ++++- .../assorted/server_selection_spec_helper.js | 9 ++++- test/unit/sdam/topology.test.ts | 37 ++++++++++--------- 4 files changed, 43 insertions(+), 21 deletions(-) diff --git a/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts b/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts index 62a545417bb..5d726206c21 100644 --- a/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts +++ b/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts @@ -215,7 +215,14 @@ describe('Server Discovery and Monitoring (spec)', function () { .callsFake(async function (_selector, _options) { topologySelectServers.restore(); - const fakeServer = { s: { state: 'connected' }, removeListener: () => true }; + const fakeServer = { + s: { state: 'connected' }, + removeListener: () => true, + pool: { + checkOut: async () => ({}), + checkIn: () => undefined + } + }; return fakeServer; }); }); diff --git a/test/unit/assorted/server_discovery_and_monitoring.test.ts b/test/unit/assorted/server_discovery_and_monitoring.test.ts index 2f6e312594f..e5fcf598e81 100644 --- a/test/unit/assorted/server_discovery_and_monitoring.test.ts +++ b/test/unit/assorted/server_discovery_and_monitoring.test.ts @@ -31,7 +31,14 @@ describe('Server Discovery and Monitoring', function () { .callsFake(async function (_selector, _options) { topologySelectServer.restore(); - const fakeServer = { s: { state: 'connected' }, removeListener: () => true }; + const fakeServer = { + s: { state: 'connected' }, + removeListener: () => true, + pool: { + checkOut: async () => ({}), + checkIn: () => undefined + } + }; return fakeServer; }); diff --git a/test/unit/assorted/server_selection_spec_helper.js b/test/unit/assorted/server_selection_spec_helper.js index 040d8fbdf10..55b890a6646 100644 --- a/test/unit/assorted/server_selection_spec_helper.js +++ b/test/unit/assorted/server_selection_spec_helper.js @@ -106,7 +106,14 @@ export async function executeServerSelectionTest(testDefinition) { .callsFake(async function () { topologySelectServers.restore(); - const fakeServer = { s: { state: 'connected' }, removeListener: () => {} }; + const fakeServer = { + s: { state: 'connected' }, + removeListener: () => true, + pool: { + checkOut: async () => ({}), + checkIn: () => undefined + } + }; return fakeServer; }); diff --git a/test/unit/sdam/topology.test.ts b/test/unit/sdam/topology.test.ts index 8fac7f52ee9..1b0e257e27e 100644 --- a/test/unit/sdam/topology.test.ts +++ b/test/unit/sdam/topology.test.ts @@ -25,6 +25,7 @@ import { Topology } from '../../../src/sdam/topology'; import { TopologyDescription } from '../../../src/sdam/topology_description'; import { TimeoutContext } from '../../../src/timeout'; import { isHello, ns } from '../../../src/utils'; +import { ConnectionPool } from '../../mongodb'; import * as mock from '../../tools/mongodb-mock/index'; import { topologyWithPlaceholderClient } from '../../tools/utils'; @@ -444,31 +445,23 @@ describe('Topology (unit)', function () { describe('selectServer()', function () { it('should schedule monitoring if no suitable server is found', async function () { - const topology = topologyWithPlaceholderClient('someserver:27019', {}); + const topology = topologyWithPlaceholderClient( + 'someserver:27019', + {}, + { serverSelectionTimeoutMS: 10 } + ); const requestCheck = sinon.stub(Server.prototype, 'requestCheck'); - // satisfy the initial connect, then restore the original method - const selectServer = sinon - .stub(Topology.prototype, 'selectServer') - .callsFake(async function () { - const server = Array.from(this.s.servers.values())[0]; - selectServer.restore(); - return server; - }); - sinon.stub(Server.prototype, 'connect').callsFake(function () { this.s.state = 'connected'; this.emit('connect'); return; }); - await topology.connect(); - const err = await topology - .selectServer(ReadPreference.secondary, { serverSelectionTimeoutMS: 1000 }) - .then( - () => null, - e => e - ); + const err = await topology.connect().then( + () => null, + e => e + ); expect(err).to.match(/Server selection timed out/); expect(err).to.have.property('reason'); // When server is created `connect` is called on the monitor. When server selection @@ -516,12 +509,20 @@ describe('Topology (unit)', function () { this.emit('connect'); }); + sinon.stub(ConnectionPool.prototype, 'checkOut').callsFake(async function () { + return {}; + }); + + sinon.stub(ConnectionPool.prototype, 'checkIn').callsFake(function (_) { + return; + }); + const toSelect = 10; let completed = 0; // methodology: // - perform 9 server selections, a few with a selector that throws an error // - ensure each selection immediately returns an empty result (gated by a boolean) - // guaranteeing tha the queue will be full before the last selection + // guaranteeing that the queue will be full before the last selection // - make one last selection, but ensure that all selections are no longer blocked from // returning their value // - verify that 10 callbacks were called From e38d620162ecaeaf4870df8f1f38a9eac0e0f4b7 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 7 Oct 2025 12:33:29 -0400 Subject: [PATCH 3/7] fix init dns tests --- .../initial_dns_seedlist_discovery.prose.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/integration/initial-dns-seedlist-discovery/initial_dns_seedlist_discovery.prose.test.ts b/test/integration/initial-dns-seedlist-discovery/initial_dns_seedlist_discovery.prose.test.ts index a4fba77f068..3f931c019c6 100644 --- a/test/integration/initial-dns-seedlist-discovery/initial_dns_seedlist_discovery.prose.test.ts +++ b/test/integration/initial-dns-seedlist-discovery/initial_dns_seedlist_discovery.prose.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import * as dns from 'dns'; import * as sinon from 'sinon'; -import { MongoAPIError, Server, ServerDescription, Topology } from '../../mongodb'; +import { ConnectionPool, MongoAPIError, Server, ServerDescription, Topology } from '../../mongodb'; import { topologyWithPlaceholderClient } from '../../tools/utils'; describe('Initial DNS Seedlist Discovery (Prose Tests)', () => { @@ -41,6 +41,14 @@ describe('Initial DNS Seedlist Discovery (Prose Tests)', () => { {} as any ); }); + + sinon.stub(ConnectionPool.prototype, 'checkOut').callsFake(async function () { + return {}; + }); + + sinon.stub(ConnectionPool.prototype, 'checkIn').callsFake(function () { + return; + }); }); afterEach(async function () { From 1c70d3a78519d76f14935d0b015689b3c5a50261 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 7 Oct 2025 12:53:18 -0400 Subject: [PATCH 4/7] docs: connect --- src/mongo_client.ts | 49 ++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 474edc44f70..fde87add710 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -594,20 +594,13 @@ export class MongoClient extends TypedEventEmitter implements } /** - * Connect to MongoDB using a url + * An optional method to verify a typical set of preconditions before using a MongoClient. + * For detailed information about the connect process see the MongoClient.connect static method documentation. * - * @remarks - * Calling `connect` is optional since the first operation you perform will call `connect` if it's needed. - * `timeoutMS` will bound the time any operation can take before throwing a timeout error. - * However, when the operation being run is automatically connecting your `MongoClient` the `timeoutMS` will not apply to the time taken to connect the MongoClient. - * This means the time to setup the `MongoClient` does not count against `timeoutMS`. - * If you are using `timeoutMS` we recommend connecting your client explicitly in advance of any operation to avoid this inconsistent execution time. - * - * @remarks - * The driver will look up corresponding SRV and TXT records if the connection string starts with `mongodb+srv://`. - * If those look ups throw a DNS Timeout error, the driver will retry the look up once. + * @param url - The MongoDB connection string (supports `mongodb://` and `mongodb+srv://` schemes) + * @param options - Optional configuration options for the client * - * @see docs.mongodb.org/manual/reference/connection-string/ + * @see https://www.mongodb.com/docs/manual/reference/connection-string/ */ async connect(): Promise { if (this.connectionLock) { @@ -868,21 +861,35 @@ export class MongoClient extends TypedEventEmitter implements } /** - * Connect to MongoDB using a url + * Creates a new MongoClient instance and immediately connects it to MongoDB. + * This convenience method combines `new MongoClient(url, options)` and `client.connect()` in a single step. + * + * Connect can be helpful to detect configuration issues early by validating: + * - **DNS Resolution**: Verifies that SRV records and hostnames in the connection string resolve DNS entries + * - **Network Connectivity**: Confirms that host addresses are reachable and ports are open + * - **TLS Configuration**: Validates SSL/TLS certificates, CA files, and encryption settings are correct + * - **Authentication**: Verifies that provided credentials are valid + * - **Server Compatibility**: Ensures the MongoDB server version is supported by this driver version + * - **Load Balancer Setup**: For load-balanced deployments, confirms the service is properly configured + * + * @returns A promise that resolves to the same MongoClient instance once connected * * @remarks - * Calling `connect` is optional since the first operation you perform will call `connect` if it's needed. - * `timeoutMS` will bound the time any operation can take before throwing a timeout error. - * However, when the operation being run is automatically connecting your `MongoClient` the `timeoutMS` will not apply to the time taken to connect the MongoClient. - * This means the time to setup the `MongoClient` does not count against `timeoutMS`. - * If you are using `timeoutMS` we recommend connecting your client explicitly in advance of any operation to avoid this inconsistent execution time. + * **Connection is Optional:** Calling `connect` is optional since any operation method (`find`, `insertOne`, etc.) + * will automatically perform these same validation steps if the client is not already connected. + * However, explicitly calling `connect` can make sense for: + * - **Fail-fast Error Detection**: Non-transient connection issues (hostname unresolved, port refused connection) are discovered immediately rather than during your first operation + * - **Predictable Performance**: Eliminates first connection overhead from your first database operation * * @remarks - * The programmatically provided options take precedence over the URI options. + * **Connection Pooling Impact:** Calling `connect` will populate the connection pool with one connection + * to a server selected by the client's configured `readPreference` (defaults to primary). * * @remarks - * The driver will look up corresponding SRV and TXT records if the connection string starts with `mongodb+srv://`. - * If those look ups throw a DNS Timeout error, the driver will retry the look up once. + * **Timeout Behavior:** When using `timeoutMS`, the connection establishment time does not count against + * the timeout for subsequent operations. This means `connect` runs without a `timeoutMS` limit, while + * your database operations will still respect the configured timeout. If you need predictable operation + * timing with `timeoutMS`, call `connect` explicitly before performing operations. * * @see https://www.mongodb.com/docs/manual/reference/connection-string/ */ From 011ecce2df6a5133d206db0f3d00bc9725d519c1 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 8 Oct 2025 14:48:53 -0400 Subject: [PATCH 5/7] chore: move fake server to utils --- test/tools/utils.ts | 11 +++++++++++ .../server_discovery_and_monitoring.spec.test.ts | 13 ++----------- .../server_discovery_and_monitoring.test.ts | 12 ++---------- test/unit/assorted/server_selection_spec_helper.js | 13 ++----------- 4 files changed, 17 insertions(+), 32 deletions(-) diff --git a/test/tools/utils.ts b/test/tools/utils.ts index 79d918a6897..c3ea662ef0f 100644 --- a/test/tools/utils.ts +++ b/test/tools/utils.ts @@ -463,6 +463,17 @@ export async function makeMultiResponseBatchModelArray( return models; } +export function fakeServer() { + return { + s: { state: 'connected' }, + removeListener: () => true, + pool: { + checkOut: async () => ({}), + checkIn: () => undefined + } + }; +} + /** * A utility to measure the duration of an async function. This is intended to be used for CSOT * testing, where we expect to timeout within a certain threshold and want to measure the duration diff --git a/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts b/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts index 5d726206c21..e84e70f7118 100644 --- a/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts +++ b/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts @@ -38,7 +38,7 @@ import { Server } from '../../../src/sdam/server'; import { ServerDescription, type TopologyVersion } from '../../../src/sdam/server_description'; import { Topology } from '../../../src/sdam/topology'; import { isRecord, ns, squashError } from '../../../src/utils'; -import { ejson } from '../../tools/utils'; +import { ejson, fakeServer } from '../../tools/utils'; const SDAM_EVENT_CLASSES = { ServerDescriptionChangedEvent, @@ -214,16 +214,7 @@ describe('Server Discovery and Monitoring (spec)', function () { .stub(Topology.prototype, 'selectServer') .callsFake(async function (_selector, _options) { topologySelectServers.restore(); - - const fakeServer = { - s: { state: 'connected' }, - removeListener: () => true, - pool: { - checkOut: async () => ({}), - checkIn: () => undefined - } - }; - return fakeServer; + return fakeServer(); }); }); diff --git a/test/unit/assorted/server_discovery_and_monitoring.test.ts b/test/unit/assorted/server_discovery_and_monitoring.test.ts index e5fcf598e81..b3db313ea7e 100644 --- a/test/unit/assorted/server_discovery_and_monitoring.test.ts +++ b/test/unit/assorted/server_discovery_and_monitoring.test.ts @@ -8,6 +8,7 @@ import { Server } from '../../../src/sdam/server'; import { ServerDescription } from '../../../src/sdam/server_description'; import { Topology } from '../../../src/sdam/topology'; import { type TopologyDescription } from '../../../src/sdam/topology_description'; +import { fakeServer } from '../../tools/utils'; describe('Server Discovery and Monitoring', function () { let serverConnect: sinon.SinonStub; @@ -30,16 +31,7 @@ describe('Server Discovery and Monitoring', function () { .stub(Topology.prototype, 'selectServer') .callsFake(async function (_selector, _options) { topologySelectServer.restore(); - - const fakeServer = { - s: { state: 'connected' }, - removeListener: () => true, - pool: { - checkOut: async () => ({}), - checkIn: () => undefined - } - }; - return fakeServer; + return fakeServer(); }); events = []; diff --git a/test/unit/assorted/server_selection_spec_helper.js b/test/unit/assorted/server_selection_spec_helper.js index 55b890a6646..a110ae3d117 100644 --- a/test/unit/assorted/server_selection_spec_helper.js +++ b/test/unit/assorted/server_selection_spec_helper.js @@ -9,7 +9,7 @@ const ServerSelectors = require('../../../src/sdam/server_selection'); const sinon = require('sinon'); const { expect } = require('chai'); -const { topologyWithPlaceholderClient } = require('../../tools/utils'); +const { fakeServer, topologyWithPlaceholderClient } = require('../../tools/utils'); export function serverDescriptionFromDefinition(definition, hosts) { hosts = hosts || []; @@ -105,16 +105,7 @@ export async function executeServerSelectionTest(testDefinition) { .stub(Topology.prototype, 'selectServer') .callsFake(async function () { topologySelectServers.restore(); - - const fakeServer = { - s: { state: 'connected' }, - removeListener: () => true, - pool: { - checkOut: async () => ({}), - checkIn: () => undefined - } - }; - return fakeServer; + return fakeServer(); }); await topology.connect(); From 67e8b573d8ff103adeb5262f49d86656344df6fc Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 8 Oct 2025 17:10:57 -0400 Subject: [PATCH 6/7] docs: typical preconditions fix --- src/mongo_client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mongo_client.ts b/src/mongo_client.ts index fde87add710..8a6f54162fb 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -594,7 +594,7 @@ export class MongoClient extends TypedEventEmitter implements } /** - * An optional method to verify a typical set of preconditions before using a MongoClient. + * An optional method to verify a handful of assumptions that are generally useful at application boot-time before using a MongoClient. * For detailed information about the connect process see the MongoClient.connect static method documentation. * * @param url - The MongoDB connection string (supports `mongodb://` and `mongodb+srv://` schemes) From 56c77a0e7ec0f5053947c3ea0b28290fd2e53e69 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 8 Oct 2025 19:02:23 -0400 Subject: [PATCH 7/7] test: combine --- .../node-specific/mongo_client.test.ts | 34 ++++--------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/test/integration/node-specific/mongo_client.test.ts b/test/integration/node-specific/mongo_client.test.ts index 05120dd3eef..435a16bb93e 100644 --- a/test/integration/node-specific/mongo_client.test.ts +++ b/test/integration/node-specific/mongo_client.test.ts @@ -610,33 +610,13 @@ describe('class MongoClient', function () { await client.close(); }); - it( - 'creates topology and checks out connection when auth is enabled', - { requires: { auth: 'enabled' } }, - async function () { - const checkoutStarted = once(client, 'connectionCheckOutStarted'); - await client.connect(); - const checkout = await checkoutStarted; - expect(checkout).to.exist; - expect(client).to.have.property('topology').that.is.instanceOf(Topology); - } - ); - - it( - 'checks out connection to confirm connectivity even when authentication is disabled', - { requires: { auth: 'disabled' } }, - async function () { - const checkoutStartedEvents = []; - client.on('connectionCheckOutStarted', event => { - checkoutStartedEvents.push(event); - }); - const checkoutStarted = once(client, 'connectionCheckOutStarted'); - await client.connect(); - const checkout = await checkoutStarted; - expect(checkout).to.exist; - expect(client).to.have.property('topology').that.is.instanceOf(Topology); - } - ); + it('creates topology and checks out connection', async function () { + const checkoutStarted = once(client, 'connectionCheckOutStarted'); + await client.connect(); + const checkout = await checkoutStarted; + expect(checkout).to.exist; + expect(client).to.have.property('topology').that.is.instanceOf(Topology); + }); it( 'permits operations to be run after connect is called',