From 7af22c9da743573df36a5e2c66ded138edb6ae42 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sat, 26 Jul 2025 12:13:59 +1200 Subject: [PATCH 01/11] Return a basic error message when DB connection is lost --- server/app.js | 2 + server/middleware/dbCheck.js | 13 +++++++ server/middleware/dbCheck.spec.js | 63 +++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 server/middleware/dbCheck.js create mode 100644 server/middleware/dbCheck.spec.js diff --git a/server/app.js b/server/app.js index 869fe6555..113f4c37d 100644 --- a/server/app.js +++ b/server/app.js @@ -32,6 +32,7 @@ import { splitTextStyleAndMetadata } from '../shared/helpers.js'; //==== Middleware Imports ====// import contentNegotiation from './middleware/content-negotiation.js'; +import dbCheck from './middleware/dbCheck.js'; import bodyParser from 'body-parser'; import cookieParser from 'cookie-parser'; import forceSSL from './forcessl.mw.js'; @@ -49,6 +50,7 @@ const sanitizeBrew = (brew, accessType)=>{ app.set('trust proxy', 1 /* number of proxies between user and server */); app.use('/', serveCompressedStaticAssets(`build`)); +app.use(dbCheck); app.use(contentNegotiation); app.use(bodyParser.json({ limit: '25mb' })); app.use(cookieParser()); diff --git a/server/middleware/dbCheck.js b/server/middleware/dbCheck.js new file mode 100644 index 000000000..e90190517 --- /dev/null +++ b/server/middleware/dbCheck.js @@ -0,0 +1,13 @@ +import mongoose from 'mongoose'; +import config from '../config.js'; + +export default (req, res, next)=>{ + // Bypass DB checks during testing + if(config.get('node_env') == 'test') return next(); + + if(mongoose.connection.readyState == 1) return next(); + return res.status(503).send({ + message : 'Unable to connect to database', + state : mongoose.connection.readyState + }); +}; diff --git a/server/middleware/dbCheck.spec.js b/server/middleware/dbCheck.spec.js new file mode 100644 index 000000000..c4482a54a --- /dev/null +++ b/server/middleware/dbCheck.spec.js @@ -0,0 +1,63 @@ +import mongoose from 'mongoose'; +import dbCheck from './dbCheck.js'; +import config from '../config.js'; + +describe('database check middleware', ()=>{ + let request; + let response; + let next; + + beforeEach(()=>{ + request = { + get : function(key) { + return this[key]; + } + }; + response = { + status : jest.fn(()=>response), + send : jest.fn(()=>{}) + }; + next = jest.fn(); + + // Mock the Config module + jest.mock('../config.js'); + config.get = jest.fn((param)=>{ + // The requested key name will be reflected to the output + return param; + }); + }); + + afterEach(()=>{ + jest.clearAllMocks(); + }); + + it('should return 503 if readystate != 1', ()=>{ + const dbState = mongoose.connection.readyState; + + mongoose.connection.readyState = 99; + + dbCheck(request, response); + + mongoose.connection.readyState = dbState; + + expect(response.status).toHaveBeenLastCalledWith(503); + expect(response.send).toHaveBeenLastCalledWith( + expect.objectContaining({ + message : 'Unable to connect to database', + state : 99 + }) + ); + }); + + it('should call next if readystate == 1', ()=>{ + const dbState = mongoose.connection.readyState; + + mongoose.connection.readyState = 1; + + dbCheck(request, response, next); + + mongoose.connection.readyState = dbState; + + expect(next).toHaveBeenCalled(); + }); +}); From 005c05376cb93f1274fd1c080a3458abe1f8d316 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sat, 26 Jul 2025 14:11:20 +1200 Subject: [PATCH 02/11] Add DB connection/disconnections events to log --- server/db.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/db.js b/server/db.js index 97da56a08..8958fa6b2 100644 --- a/server/db.js +++ b/server/db.js @@ -22,12 +22,21 @@ const handleConnectionError = (error)=>{ } }; +const addListeners = (conn)=>{ + conn.connection.on('disconnecting', ()=>{console.log('Mongo disconnecting...');}); + conn.connection.on('disconnected', ()=>{console.log('Mongo disconnected!');}); + conn.connection.on('connecting', ()=>{console.log('Mongo connecting...');}); + conn.connection.on('connected', ()=>{console.log('Mongo connected!');}); + return conn; +}; + const disconnect = async ()=>{ return await Mongoose.disconnect(); }; const connect = async (config)=>{ return await Mongoose.connect(getMongoDBURL(config), { retryWrites: false }) + .then(addListeners(Mongoose)) .catch((error)=>handleConnectionError(error)); }; @@ -35,3 +44,4 @@ export default { connect, disconnect }; + From 4fca207e0eae202269a245acd2e3508c7bcf337b Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 2 Sep 2025 20:58:08 +1200 Subject: [PATCH 03/11] Change error result to use Error Page --- client/homebrew/pages/errorPage/errors/errorIndex.js | 6 ++++++ server/middleware/dbCheck.js | 10 ++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/client/homebrew/pages/errorPage/errors/errorIndex.js b/client/homebrew/pages/errorPage/errors/errorIndex.js index c0220b648..89abd570f 100644 --- a/client/homebrew/pages/errorPage/errors/errorIndex.js +++ b/client/homebrew/pages/errorPage/errors/errorIndex.js @@ -196,6 +196,12 @@ const errorIndex = (props)=>{ **Brew ID:** ${props.brew.brewId}`, + // Database Connection Lost + '13' : dedent` + ## Database connection has been lost. + + The server could not communicate with the database.`, + //account page when account is not defined '50' : dedent` ## You are not signed in diff --git a/server/middleware/dbCheck.js b/server/middleware/dbCheck.js index e90190517..154ab4a9c 100644 --- a/server/middleware/dbCheck.js +++ b/server/middleware/dbCheck.js @@ -6,8 +6,10 @@ export default (req, res, next)=>{ if(config.get('node_env') == 'test') return next(); if(mongoose.connection.readyState == 1) return next(); - return res.status(503).send({ - message : 'Unable to connect to database', - state : mongoose.connection.readyState - }); + throw { + HBErrorCode : 13, + name : 'Database Connection Error', + message : 'Unable to connect to database', + status : mongoose.connection.readyState + }; }; From d6a7d0272af704552aa80f6852e013ba87e05174 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 2 Sep 2025 21:08:15 +1200 Subject: [PATCH 04/11] Update dbCheck test --- server/middleware/dbCheck.spec.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/server/middleware/dbCheck.spec.js b/server/middleware/dbCheck.spec.js index c4482a54a..dea218918 100644 --- a/server/middleware/dbCheck.spec.js +++ b/server/middleware/dbCheck.spec.js @@ -36,17 +36,9 @@ describe('database check middleware', ()=>{ mongoose.connection.readyState = 99; - dbCheck(request, response); + expect(()=>{dbCheck(request, response);}).toThrow(new Error('Unable to connect to database')); mongoose.connection.readyState = dbState; - - expect(response.status).toHaveBeenLastCalledWith(503); - expect(response.send).toHaveBeenLastCalledWith( - expect.objectContaining({ - message : 'Unable to connect to database', - state : 99 - }) - ); }); it('should call next if readystate == 1', ()=>{ From f141c0bebd2b8bb8aec146b3764cabf926642bad Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Thu, 2 Oct 2025 19:28:10 +1300 Subject: [PATCH 05/11] Move dbCheck to only API calls that touch the database --- server/app.js | 2 -- server/homebrew.api.js | 13 +++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/server/app.js b/server/app.js index 162b34df3..afba0997b 100644 --- a/server/app.js +++ b/server/app.js @@ -32,7 +32,6 @@ import { splitTextStyleAndMetadata } from '../shared/helpers.js'; //==== Middleware Imports ====// import contentNegotiation from './middleware/content-negotiation.js'; -import dbCheck from './middleware/dbCheck.js'; import bodyParser from 'body-parser'; import cookieParser from 'cookie-parser'; import forceSSL from './forcessl.mw.js'; @@ -50,7 +49,6 @@ const sanitizeBrew = (brew, accessType)=>{ app.set('trust proxy', 1 /* number of proxies between user and server */); app.use('/', serveCompressedStaticAssets(`build`)); -app.use(dbCheck); app.use(contentNegotiation); app.use(bodyParser.json({ limit: '25mb' })); app.use(cookieParser()); diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 3221638ab..5a3008cc9 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -13,6 +13,7 @@ import { md5 } from 'hash-wasm'; import { splitTextStyleAndMetadata, brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js'; import checkClientVersion from './middleware/check-client-version.js'; +import dbCheck from './middleware/dbCheck.js'; const router = express.Router(); @@ -530,11 +531,11 @@ const api = { } }; -router.post('/api', checkClientVersion, asyncHandler(api.newBrew)); -router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); -router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); -router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew)); -router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew)); -router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle)); +router.post('/api', checkClientVersion, dbCheck, asyncHandler(api.newBrew)); +router.put('/api/:id', checkClientVersion, dbCheck, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); +router.put('/api/update/:id', checkClientVersion, dbCheck, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); +router.delete('/api/:id', checkClientVersion, dbCheck, asyncHandler(api.deleteBrew)); +router.get('/api/remove/:id', checkClientVersion, dbCheck, asyncHandler(api.deleteBrew)); +router.get('/api/theme/:renderer/:id', dbCheck, asyncHandler(api.getThemeBundle)); export default api; From 2b138e56db72c1770a98d462e5aa3f54b2297f43 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sat, 4 Oct 2025 12:29:06 +1300 Subject: [PATCH 06/11] Add dbCheck middleware to app.js routes that use DB --- server/app.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/server/app.js b/server/app.js index afba0997b..673d70c68 100644 --- a/server/app.js +++ b/server/app.js @@ -35,6 +35,7 @@ import contentNegotiation from './middleware/content-negotiation.js'; import bodyParser from 'body-parser'; import cookieParser from 'cookie-parser'; import forceSSL from './forcessl.mw.js'; +import dbCheck from './middleware/dbCheck.js'; const sanitizeBrew = (brew, accessType)=>{ @@ -274,7 +275,7 @@ app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{ app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);}); //User Page -app.get('/user/:username', async (req, res, next)=>{ +app.get('/user/:username', dbCheck, async (req, res, next)=>{ const ownAccount = req.account && (req.account.username == req.params.username); req.ogMeta = { ...defaultMetaTags, @@ -346,7 +347,7 @@ app.get('/user/:username', async (req, res, next)=>{ }); //Change author name on brews -app.put('/api/user/rename', async (req, res)=>{ +app.put('/api/user/rename', dbCheck, async (req, res)=>{ const { username, newUsername } = req.body; const ownAccount = req.account && (req.account.username == newUsername); @@ -432,7 +433,7 @@ app.get('/new', asyncHandler(async(req, res, next)=>{ })); //Share Page -app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{ +app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{ const { brew } = req; req.ogMeta = { ...defaultMetaTags, title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`, @@ -459,7 +460,7 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r })); //Account Page -app.get('/account', asyncHandler(async (req, res, next)=>{ +app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{ const data = {}; data.title = 'Account Information Page'; From bb0a840113e2c246d11cd41ca5a07b95b48d8c36 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 5 Oct 2025 15:30:47 +1300 Subject: [PATCH 07/11] Fix HBErrorCode to string, not number --- server/middleware/dbCheck.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/middleware/dbCheck.js b/server/middleware/dbCheck.js index 154ab4a9c..f486eab52 100644 --- a/server/middleware/dbCheck.js +++ b/server/middleware/dbCheck.js @@ -7,7 +7,7 @@ export default (req, res, next)=>{ if(mongoose.connection.readyState == 1) return next(); throw { - HBErrorCode : 13, + HBErrorCode : '13', name : 'Database Connection Error', message : 'Unable to connect to database', status : mongoose.connection.readyState From a14ea8956237628d954bb0f5c8c03b59ad8f8559 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 5 Oct 2025 15:31:04 +1300 Subject: [PATCH 08/11] Post-merge cleanup --- server/db.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/db.js b/server/db.js index d7baa922d..4930e4cd6 100644 --- a/server/db.js +++ b/server/db.js @@ -39,8 +39,8 @@ const connect = async (config)=>{ retryWrites : false, autoIndex : (config.get('local_environments').includes(config.get('node_env'))) }) - .then(addListeners(Mongoose)) - .catch((error)=>handleConnectionError(error)); + .then(addListeners(Mongoose)) + .catch((error)=>handleConnectionError(error)); }; export default { From 88bc9b79c9f1ed54182dfa5ddaf73d18ea4d4744 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 5 Oct 2025 15:31:53 +1300 Subject: [PATCH 09/11] Add new error message to nav bar error container --- client/homebrew/navbar/error-navitem.jsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/client/homebrew/navbar/error-navitem.jsx b/client/homebrew/navbar/error-navitem.jsx index 6d9bec444..030378cb6 100644 --- a/client/homebrew/navbar/error-navitem.jsx +++ b/client/homebrew/navbar/error-navitem.jsx @@ -2,9 +2,9 @@ require('./error-navitem.less'); const React = require('react'); const Nav = require('naturalcrit/nav/nav.jsx'); -const ErrorNavItem = ({error = '', clearError})=>{ +const ErrorNavItem = ({ error = '', clearError })=>{ const response = error.response; - const errorCode = error.code + const errorCode = error.code; const status = response?.status; const HBErrorCode = response?.body?.HBErrorCode; const message = response?.body?.message; @@ -112,6 +112,15 @@ const ErrorNavItem = ({error = '', clearError})=>{ ; } + if(HBErrorCode === '13') { + return + Oops! +
+ Server has lost connection to the database. +
+
; + } + if(errorCode === 'ECONNABORTED') { return Oops! From c17a5e72b92ea2f35d9dd971a7f4b5d37732e21f Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 5 Oct 2025 15:40:40 +1300 Subject: [PATCH 10/11] Switch to Calc's simplified test spec --- server/middleware/dbCheck.spec.js | 67 +++++++++---------------------- 1 file changed, 20 insertions(+), 47 deletions(-) diff --git a/server/middleware/dbCheck.spec.js b/server/middleware/dbCheck.spec.js index dea218918..0c37d40ab 100644 --- a/server/middleware/dbCheck.spec.js +++ b/server/middleware/dbCheck.spec.js @@ -2,54 +2,27 @@ import mongoose from 'mongoose'; import dbCheck from './dbCheck.js'; import config from '../config.js'; -describe('database check middleware', ()=>{ - let request; - let response; - let next; +describe('dbCheck middleware', ()=>{ + const next = jest.fn(); - beforeEach(()=>{ - request = { - get : function(key) { - return this[key]; - } - }; - response = { - status : jest.fn(()=>response), - send : jest.fn(()=>{}) - }; - next = jest.fn(); - - // Mock the Config module - jest.mock('../config.js'); - config.get = jest.fn((param)=>{ - // The requested key name will be reflected to the output - return param; - }); - }); - - afterEach(()=>{ - jest.clearAllMocks(); - }); - - it('should return 503 if readystate != 1', ()=>{ - const dbState = mongoose.connection.readyState; - - mongoose.connection.readyState = 99; - - expect(()=>{dbCheck(request, response);}).toThrow(new Error('Unable to connect to database')); - - mongoose.connection.readyState = dbState; - }); - - it('should call next if readystate == 1', ()=>{ - const dbState = mongoose.connection.readyState; - - mongoose.connection.readyState = 1; - - dbCheck(request, response, next); - - mongoose.connection.readyState = dbState; + afterEach(()=>jest.clearAllMocks()); + it('should skip check in test mode', ()=>{ + config.get = jest.fn(()=>'test'); + expect(()=>dbCheck({}, {}, next)).not.toThrow(); expect(next).toHaveBeenCalled(); }); -}); + + it('should call next if readyState == 1', ()=>{ + config.get = jest.fn(()=>'production'); + mongoose.connection.readyState = 1; + dbCheck({}, {}, next); + expect(next).toHaveBeenCalled(); + }); + + it('should throw if readyState != 1', ()=>{ + config.get = jest.fn(()=>'production'); + mongoose.connection.readyState = 99; + expect(()=>dbCheck({}, {}, next)).toThrow(/Unable to connect/); + }); +}); \ No newline at end of file From 0d38b8607e1e4735bf426402c7f6525b521ad5b2 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Mon, 6 Oct 2025 18:51:05 +1300 Subject: [PATCH 11/11] Shift to router.use(dbCheck) instead of defining on every route --- server/homebrew.api.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/server/homebrew.api.js b/server/homebrew.api.js index fc1a4c70e..c931cb657 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -531,11 +531,13 @@ const api = { } }; -router.post('/api', checkClientVersion, dbCheck, asyncHandler(api.newBrew)); -router.put('/api/:id', checkClientVersion, dbCheck, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); -router.put('/api/update/:id', checkClientVersion, dbCheck, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); -router.delete('/api/:id', checkClientVersion, dbCheck, asyncHandler(api.deleteBrew)); -router.get('/api/remove/:id', checkClientVersion, dbCheck, asyncHandler(api.deleteBrew)); -router.get('/api/theme/:renderer/:id', dbCheck, asyncHandler(api.getThemeBundle)); +router.use(dbCheck); + +router.post('/api', checkClientVersion, asyncHandler(api.newBrew)); +router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); +router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); +router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew)); +router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew)); +router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle)); export default api;