Skip to content

Mongoose connection is getting force closed unintentionally #15531

@domharrington

Description

@domharrington

Prerequisites

  • I have written a descriptive issue title
  • 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:

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:

/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 in the code:

console.log('this.otherDbs', this.otherDbs.map(db => db.name));
  1. When we call connection.close(), it hits this code path:

mongoose/lib/connection.js

Lines 1203 to 1226 in 0a6a199

Connection.prototype.close = async function close(force) {
if (typeof force === 'function' || (arguments.length === 2 && typeof arguments[1] === 'function')) {
throw new MongooseError('Connection.prototype.close() no longer accepts a callback');
}
if (force != null && typeof force === 'object') {
this.$wasForceClosed = !!force.force;
} else {
this.$wasForceClosed = !!force;
}
if (this._lastHeartbeatAt != null) {
this._lastHeartbeatAt = null;
}
for (const model of Object.values(this.models)) {
// If manually disconnecting, make sure to clear each model's `$init`
// promise, so Mongoose knows to re-run `init()` in case the
// connection is re-opened. See gh-12047.
delete model.$init;
}
return this._close(force, false);
};

  1. This internally calls this._close():

Connection.prototype._close = async function _close(force, destroy) {

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

this.onClose(force);

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

this._destroyCalled ? db.destroy({ force: force, skipCloseClient: true }) : db.close({ force: force, skipCloseClient: true });

This time with the following arguments:

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

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

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:

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:

// 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:

mongoose/lib/connection.js

Lines 1208 to 1210 in 0a6a199

if (force != null && typeof force === 'object') {
this.$wasForceClosed = !!force.force;
} else {

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":

const error = new MongooseError('Connection was force closed');

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:

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:

// push onto the otherDbs stack, this is used when state changes
if (options.noListener !== true) {
this.otherDbs.push(newConn);
}

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    confirmed-bugWe've confirmed this is a bug in Mongoose and will fix it.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions