diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c0b9465f..217992cae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ * Fixed race conditions in the `setText`, `appendText`, and `restoreRevision` functions (HTTP API). * Fixed a crash if the database is busy enough to cause a query timeout. +* New `/health` endpoint for getting information about Etherpad's health (see + [draft-inadarei-api-health-check-06](https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html)). #### For plugin authors diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js index 8aab5ce4f..bf23487c2 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -11,6 +11,16 @@ const util = require('util'); const webaccess = require('./webaccess'); exports.expressPreSession = async (hookName, {app}) => { + // This endpoint is intended to conform to: + // https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html + app.get('/health', (req, res) => { + res.set('Content-Type', 'application/health+json'); + res.json({ + status: 'pass', + releaseId: settings.getEpVersion(), + }); + }); + app.get('/stats', (req, res) => { res.json(require('../../stats').toJSON()); }); diff --git a/src/tests/backend/specs/health.js b/src/tests/backend/specs/health.js new file mode 100644 index 000000000..0090aedbb --- /dev/null +++ b/src/tests/backend/specs/health.js @@ -0,0 +1,56 @@ +'use strict'; + +const assert = require('assert').strict; +const common = require('../common'); +const settings = require('../../../node/utils/Settings'); +const superagent = require('superagent'); + +describe(__filename, function () { + let agent; + const backup = {}; + + const getHealth = () => agent.get('/health') + .accept('application/health+json') + .buffer(true) + .parse(superagent.parse['application/json']) + .expect(200) + .expect((res) => assert.equal(res.type, 'application/health+json')); + + before(async function () { + agent = await common.init(); + }); + + beforeEach(async function () { + backup.settings = {}; + for (const setting of ['requireAuthentication', 'requireAuthorization']) { + backup.settings[setting] = settings[setting]; + } + }); + + afterEach(async function () { + Object.assign(settings, backup.settings); + }); + + it('/health works', async function () { + const res = await getHealth(); + assert.equal(res.body.status, 'pass'); + assert.equal(res.body.releaseId, settings.getEpVersion()); + }); + + it('auth is not required', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + const res = await getHealth(); + assert.equal(res.body.status, 'pass'); + }); + + // We actually want to test that no express-session state is created, but that is difficult to do + // without intrusive changes or unpleasant ueberdb digging. Instead, we assume that the lack of a + // cookie means that no express-session state was created (how would express-session look up the + // session state if no ID was returned to the client?). + it('no cookie is returned', async function () { + const res = await getHealth(); + const cookie = res.headers['set-cookie']; + assert(cookie == null, `unexpected Set-Cookie: ${cookie}`); + }); +});