getindiekit / indiekit

The little Node.js server with all the parts needed to publish content to your personal website and share it on social networks.
https://getindiekit.com
MIT License
341 stars 37 forks source link

Immediately notify the user about connection issues with MongoDB #690

Closed jackdbd closed 9 months ago

jackdbd commented 9 months ago

Is your feature request related to a problem?

At the moment the getMongodbClient function doesn't implement any error handling, apart from printing a warning when it cannot instantiate MongoClient. This catch captures only a few failure modes, for instance when the uri scheme is incorrect, like "incorrect-mongodb-uri".

The correct implementation leaves out many failures modes which manifest as runtime issues when using Indiekit. See the snippet below for details.

Describe the solution you’d like

A UI element like a toast, a dialog, or something else could notify the user about database connection issues as soon as Indiekit starts.

I can think of the following scenarios which Indiekit should handle:

  1. Incorrect uri scheme. Fails at step 1. This is handled by the current implementation, and a warning is printed to console.
  2. Correct uri scheme but it includes a port, which in some cases cannot actually be included (mongodb+srv cannot include a port).
  3. Correct uri scheme but incorrect MongoDB credentials.
  4. Correct uri scheme and correct MongoDB credentials, but nonexistent database.
import { MongoClient, ServerApiVersion } from "mongodb";
import { uri } from "./config.js";

const username = "john";
const password = "wrong-password";
const host = "cluster123.abc.mongodb.net";
const port = 27017;

// 1. incorrect uri scheme. Fails at step 1
// const uri = "incorrect-mongodb-uri";

// 2. correct uri scheme but it fails because mongodb+srv cannot include a port. Fails at step 1
// const uri = `mongodb+srv://${username}:${password}@${host}:${port}`;

// 3. correct uri scheme but nonexistent database. Fails at step 2
// const uri = `mongodb+srv://${username}:${password}@${host}`;

// 4. correct uri (which I have in config.js)

// 5. correct uri and credentials, but nonexistent database
// const db_name = "nonexistent-database";
const db_name = "indiekit";

const run = async () => {
  // step 1: instantiate MongoClient
  let client;
  try {
    client = new MongoClient(uri, {
      serverApi: {
        deprecationErrors: true,
        strict: true,
        version: ServerApiVersion.v1,
      },
      //   serverSelectionTimeoutMS: 1, // this will let us instantiate MongoClient, but it will fail at step 2
      serverSelectionTimeoutMS: 10000,
    });
  } catch (ex) {
    // Example: Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"
    // Example: mongodb+srv URI cannot have port number

    // There is no db connection to close because there is no client in the first place
    return {
      error: new Error(ex.message || "could not create MondoDB client"),
    };
  }

  // step 2: connect to MongoDB
  let error;
  try {
    await client.connect();
  } catch (ex) {
    // Example: querySrv ENOTFOUND _mongodb._tcp.cluster123.abc.mongodb.net
    // Example: MongoServerSelectionError: Server selection timed out after 1 ms
    error = new Error(ex.message || "could not connect to MongoDB");
  }

  if (error) {
    await client.close();
    return { error };
  }

  let db_names = [];
  try {
    db_names = (await client.db(db_name).admin().listDatabases()).databases.map(
      (db) => db.name
    );
    if (!db_names.includes(db_name)) {
      error = new Error(`database ${db_name} does not exist`);
    }
  } catch (ex) {
    error = new Error(
      ex.message ||
        `could connect to MongDB but could not connect to database ${db_name}`
    );
  }

  if (error) {
    await client.close();
    return { error };
  }

  // Database exists, so we return the MongoDB client. The caller will have to
  // call `await client.close()` when appropriate.
  return { value: client };
};

run().then((result) => {
  const { error, value: client } = result;

  if (error) {
    console.trace(error);
  }

  if (client) {
    client.close().then(() => {
      console.info(`connection closed`);
    });
  }
});

Describe alternatives you’ve considered

No response

Additional context

I'm writing here how I found out about the need for better error handling in regards to the MongoDB connection.

I was using MongoDB Atlas as my database, and I was connecting to it from my laptop and from a Google Cloud Compute Engine VM.

The connection from my laptop was fine, and I could use Indiekit without any issue. However, when launching Indiekit from the VM, I had these issues:

  1. Indiekit would take ~30 seconds to start
  2. I could not create notes, nor query them
  3. I could not upload media, nor query them

Long story short, the reason for those ~30 seconds was that MongoClient could not establish a connection to Atlas, and would throw after 30000ms (the default value for serverSelectionTimeoutMS). Indiekit printed a warning but kept running. This means that client was now undefined, and I encountered runtime exceptions when executing posts.insertOne() or media.findOne(), because posts and media were undefined.

And why wasn't I able to connect to Atlas from my VM? Simple. I had forgotten about whitelisting the IP of my VM in Atlas. :man_facepalming:

jackdbd commented 9 months ago

If Indiekit cannot establish a connection to MongoDB at startup, I get an error like this as soon as I try to perform some operation on the database.

upload-file-error