Repository: Automattic/mongoose (JavaScript)

Issue:
Mongoose connection is getting force closed unintentionally
### Prerequisites

- [x] I have written a descriptive issue title
- [x] I have searched existing issues to ensure the bug has not already been reported


### Mongoose version

8.16.3

### Node.js version

20.x

### MongoDB server version

7.0.15

### Typescript version (if applicable)

_No response_

### Description

Calling `mongoose.disconnect()` or `mongoose.connection.close()` when there's another connection created via `useDb()` causes the current mongoose instance to be marked as force closed and then becomes completely unusable.

### Steps to Reproduce

Consider the following code:

```js
import mongoose from 'mongoose';

const schema = new mongoose.Schema({
  name: String,
  age: Number,
});
const User = mongoose.model('User', schema);

mongoose.connect('mongodb://localhost:27017/mydatabase');
const anotherConnection = mongoose.connection.useDb('anotherDatabase');

console.log(await User.create({ name: 'Alice', age: 30 }));

await mongoose.connection.close()
await mongoose.connect('mongodb://localhost:27017/mydatabase');

console.log(await User.findOne({ name: 'Alice', age: 30 }));
```

The last call to `findOne()` errors with the following:

```js
/private/tmp/mongoose-force-connection/node_modules/mongoose/lib/drivers/node-mongodb-native/collection.js:115
      const error = new MongooseError('Connection was force closed');
                    ^

MongooseError: Connection was force closed
    at NativeCollection.<computed> [as findOne] (/private/tmp/mongoose-force-connection/node_modules/mongoose/lib/drivers/node-mongodb-native/collection.js:115:21)
    at model.Query._findOne (/private/tmp/mongoose-force-connection/node_modules/mongoose/lib/query.js:2687:45)
    at model.Query.exec (/private/tmp/mongoose-force-connection/node_modules/mongoose/lib/query.js:4604:80)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async <anonymous> (/private/tmp/mongoose-force-connection/index.ts:17:13)

Node.js v20.19.0
```

The issue is also present when using `mongoose.disconnect()` instead of `mongoose.connection.close()`.

---------

I spent some time on Friday following the code through and I've figured out why this is happening: it is related to `useDb()` and the internal creation of the `otherDbs` storage. To help with debugging this, I've added the following console.log statement [here](https://github.com/Automattic/mongoose/blob/0a6a199d3f26e8095bb0a9145788a935b7657032/lib/connection.js#L1326) in the code:

```js
console.log('this.otherDbs', this.otherDbs.map(db => db.name));
```

1. When we call connection.close(), it hits this code path:

https://github.com/Automattic/mongoose/blob/0a6a199d3f26e8095bb0a9145788a935b7657032/lib/connection.js#L1203-L1226

2. This internally calls `this._close()`:

https://github.com/Automattic/mongoose/blob/0a6a199d3f26e8095bb0a9145788a935b7657032/lib/connection.js#L1236

If you are already connected (which we are), that comes through into here and calls `this.onClose()`:

https://github.com/Automattic/mongoose/blob/0a6a199d3f26e8095bb0a9145788a935b7657032/lib/connection.js#L1264

3. Then `onClose` interates through `this.otherDbs` and calls close again:

https://github.com/Automattic/mongoose/blob/0a6a199d3f26e8095bb0a9145788a935b7657032/lib/connection.js#L1328

This time with the following arguments:

```js
db.close({ force: force, skipCloseClient: true })
```

Log output is this, showing `otherDbs` as the second connection:

```js
this.otherDbs [ 'second-database' ]
```

This loops us back around to step 1 again, then when we get to step 3, we get this log output:

```js
this.otherDbs [ 'first-database' ]
```

--------
I believe this to be the source of the bug. connection.close() is getting called 3 times in total, the 3rd and final time is being marked as a force close. The calls to close look like this:

```js
// This is the only one from my code
mongoose.connection.close()

// The first time `onClose`, force: undefined because it wasn't passed to `close()` originally
// The `db` instance here is the `second-database` connection
db.close({ force: undefined, skipCloseClient: true })

// The second time `onClose`, force value is nested due to the previous call
// The `db` instance here is the `first-database` connection again
db.close({ force: { force: undefined, skipCloseClient: true }, skipCloseClient: true })
```

You can see the `force` property from the 2nd call is getting passed through and nested on the 3rd. It's this final call that is causing this conditional to be true:

https://github.com/Automattic/mongoose/blob/0a6a199d3f26e8095bb0a9145788a935b7657032/lib/connection.js#L1208-L1210

Which finally sets `this.$wasForceClosed = true`. Any subsequent attempt to use mongoose methods then hit this line in the driver saying that "Connection was force closed":

https://github.com/Automattic/mongoose/blob/0a6a199d3f26e8095bb0a9145788a935b7657032/lib/drivers/node-mongodb-native/collection.js#L115

### Expected Behavior

The above code should work as expected, `$wasForceClosed` should not be getting set to `true` when it is not provided to `mongoose.connection.close()`.

I have found a workaround which is to provide `noListener: true` as an option to the `useDb()` call:

```js
const anotherConnection = mongoose.connection.useDb('second-database', { noListener: true });
```

This in effect just means the second database connection never gets saved into `otherDbs` at all:

https://github.com/Automattic/mongoose/blob/0a6a199d3f26e8095bb0a9145788a935b7657032/lib/drivers/node-mongodb-native/connection.js#L121-L124

This works for my case, but this seems like a bug in mongoose.



Discussion:

Patch:
diff --git a/lib/connection.js b/lib/connection.js
index 919213fdb09..5a8b2c8ca55 100644
--- a/lib/connection.js
+++ b/lib/connection.js
@@ -1324,8 +1324,10 @@ Connection.prototype.onClose = function onClose(force) {
 
   this.emit('close', force);
 
+  const wasForceClosed = typeof force === 'object' && force !== null ? force.force : force;
+
   for (const db of this.otherDbs) {
-    this._destroyCalled ? db.destroy({ force: force, skipCloseClient: true }) : db.close({ force: force, skipCloseClient: true });
+    this._destroyCalled ? db.destroy({ force: wasForceClosed, skipCloseClient: true }) : db.close({ force: wasForceClosed, skipCloseClient: true });
   }
 };
 


Test patch:
diff --git a/test/connection.test.js b/test/connection.test.js
index 98919402d41..d996b42e965 100644
--- a/test/connection.test.js
+++ b/test/connection.test.js
@@ -463,6 +463,18 @@ describe('connections:', function() {
     }
   });
 
+  it('can re-open after close with useDb() (gh-15531)', async function() {
+    const opts = {};
+    const conn = await mongoose.createConnection(start.uri, opts).asPromise();
+
+    conn.useDb('test-db');
+
+    await conn.close();
+    await conn.openUri(start.uri);
+    assert.strictEqual(conn.readyState, 1);
+    await conn.collection('Test').insertOne({ x: 1 });
+  });
+
   it('verify that attempt to re-open destroyed connection throws error, via callback', async function() {
     const opts = {};
     const conn = await mongoose.createConnection(start.uri, opts).asPromise();