Skip to content

Commit

Permalink
feat(status): custom status codes (#190)
Browse files Browse the repository at this point in the history
* feat(status): custom status codes

* revert(binary) revert lockfile change

* Custom status (#191)

* Revert "feat(status): custom status codes"

This reverts commit 522df64.

* Added statusOk and statusError to configuration

The new configuration properties: statusOk and statusError will allow to globally configure the HTTP codes, used to respond with on a successful or an unsuccessful healthcheck respectively.
An unsuccessful healthcheck may override the global statusError property by setting the statusCode property on the Error object.

* Added typings for statusOk and statusError

Co-authored-by: rxmarbles <rmarkins@gmail.com>
Co-authored-by: Matanel Sindilevich <sindilevich@users.noreply.github.com>
  • Loading branch information
3 people committed Jun 4, 2021
1 parent 04191cc commit ee94766
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 21 deletions.
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,23 +52,26 @@ const options = {
// health check options
healthChecks: {
'/healthcheck': healthCheck, // a function accepting a state and returning a promise indicating service health,
verbatim: true, // [optional = false] use object returned from /healthcheck verbatim in response,
verbatim: true, // [optional = false] use object returned from /healthcheck verbatim in response,
__unsafeExposeStackTraces: true // [optional = false] return stack traces in error response if healthchecks throw errors
},
caseInsensitive, // [optional] whether given health checks routes are case insensitive (defaults to false)
caseInsensitive, // [optional] whether given health checks routes are case insensitive (defaults to false)

statusOk, // [optional = 200] status to be returned for successful healthchecks
statusError, // [optional = 503] status to be returned for unsuccessful healthchecks

// cleanup options
timeout: 1000, // [optional = 1000] number of milliseconds before forceful exiting
signal, // [optional = 'SIGTERM'] what signal to listen for relative to shutdown
signals, // [optional = []] array of signals to listen for relative to shutdown
sendFailuresDuringShutdown, // [optional = true] whether or not to send failure (503) during shutdown
beforeShutdown, // [optional] called before the HTTP server starts its shutdown
onSignal, // [optional] cleanup function, returning a promise (used to be onSigterm)
onShutdown, // [optional] called right before exiting
onSendFailureDuringShutdown, // [optional] called before sending each 503 during shutdowns
timeout: 1000, // [optional = 1000] number of milliseconds before forceful exiting
signal, // [optional = 'SIGTERM'] what signal to listen for relative to shutdown
signals, // [optional = []] array of signals to listen for relative to shutdown
sendFailuresDuringShutdown, // [optional = true] whether or not to send failure (503) during shutdown
beforeShutdown, // [optional] called before the HTTP server starts its shutdown
onSignal, // [optional] cleanup function, returning a promise (used to be onSigterm)
onShutdown, // [optional] called right before exiting
onSendFailureDuringShutdown, // [optional] called before sending each 503 during shutdowns

// both
logger // [optional] logger function to be called with errors. Example logger call: ('error happened during shutdown', error). See terminus.js for more details.
logger // [optional] logger function to be called with errors. Example logger call: ('error happened during shutdown', error). See terminus.js for more details.
};

createTerminus(server, options);
Expand Down
24 changes: 14 additions & 10 deletions lib/terminus.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ function noopResolves () {
return Promise.resolve()
}

async function sendSuccess (res, { info, verbatim }) {
res.statusCode = 200
async function sendSuccess (res, { info, verbatim, statusOk }) {
res.statusCode = statusOk
res.setHeader('Content-Type', 'application/json')
if (info) {
return res.end(
Expand All @@ -34,7 +34,7 @@ async function sendSuccess (res, { info, verbatim }) {
}

async function sendFailure (res, options) {
const { error, onSendFailureDuringShutdown, exposeStackTraces } = options
const { error, onSendFailureDuringShutdown, exposeStackTraces, statusCode, statusError } = options

function replaceErrors (_, value) {
if (value instanceof Error) {
Expand All @@ -54,7 +54,7 @@ async function sendFailure (res, options) {
if (onSendFailureDuringShutdown) {
await onSendFailureDuringShutdown()
}
res.statusCode = 503
res.statusCode = statusCode || statusError
res.setHeader('Content-Type', 'application/json')
if (error) {
return res.end(JSON.stringify({
Expand All @@ -73,24 +73,24 @@ const intialState = {
function noop () {}

function decorateWithHealthCheck (server, state, options) {
const { healthChecks, logger, onSendFailureDuringShutdown, sendFailuresDuringShutdown, caseInsensitive } = options
const { healthChecks, logger, onSendFailureDuringShutdown, sendFailuresDuringShutdown, caseInsensitive, statusOk, statusError } = options

let hasSetHandler = false
const createHandler = (listener) => {
const check = hasSetHandler
? () => {}
: async (healthCheck, res) => {
if (state.isShuttingDown && sendFailuresDuringShutdown) {
return sendFailure(res, { onSendFailureDuringShutdown })
return sendFailure(res, { onSendFailureDuringShutdown, statusError })
}
let info
try {
info = await healthCheck({ state })
} catch (error) {
logger('healthcheck failed', error)
return sendFailure(res, { error: error.causes, exposeStackTraces: healthChecks.__unsafeExposeStackTraces })
return sendFailure(res, { error: error.causes, exposeStackTraces: healthChecks.__unsafeExposeStackTraces, statusCode: error.statusCode, statusError })
}
return sendSuccess(res, { info, verbatim: healthChecks.verbatim })
return sendSuccess(res, { info, verbatim: healthChecks.verbatim, statusOk })
}

hasSetHandler = true
Expand Down Expand Up @@ -162,7 +162,9 @@ function terminus (server, options = {}) {
onShutdown = noopResolves,
beforeShutdown = noopResolves,
logger = noop,
caseInsensitive = false
caseInsensitive = false,
statusOk = 200,
statusError = 503
} = options
const onSignal = options.onSignal || options.onSigterm || noopResolves
const state = Object.assign({}, intialState)
Expand All @@ -173,7 +175,9 @@ function terminus (server, options = {}) {
logger,
sendFailuresDuringShutdown,
onSendFailureDuringShutdown,
caseInsensitive
caseInsensitive,
statusOk,
statusError
})
}

Expand Down
97 changes: 97 additions & 0 deletions lib/terminus.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,27 @@ describe('Terminus', () => {
expect(onHealthCheckRan).to.eql(true)
})

it('returns custom status code on resolve', async () => {
let onHealthCheckRan = false

createTerminus(server, {
healthChecks: {
'/health': () => {
onHealthCheckRan = true
return Promise.resolve()
}
},
statusOk: 201
})
server.listen(8000)

const response = await fetch('http://localhost:8000/health')
expect(response.status).to.eql(201)
expect(response.headers.has('Content-Type')).to.eql(true)
expect(response.headers.get('Content-Type')).to.eql('application/json')
expect(onHealthCheckRan).to.eql(true)
})

it('case sensitive by default', async () => {
let onHealthCheckRan = false

Expand Down Expand Up @@ -216,6 +237,82 @@ describe('Terminus', () => {
expect(loggerRan).to.eql(true)
})

it('returns global custom status code on reject', async () => {
let onHealthCheckRan = false
let loggerRan = false

createTerminus(server, {
healthChecks: {
'/health': () => {
onHealthCheckRan = true
const error = new Error()
return Promise.reject(error)
}
},
logger: () => {
loggerRan = true
},
statusError: 500
})
server.listen(8000)

const res = await fetch('http://localhost:8000/health')
expect(res.status).to.eql(500)
expect(onHealthCheckRan).to.eql(true)
expect(loggerRan).to.eql(true)
})

it('returns custom status code on reject', async () => {
let onHealthCheckRan = false
let loggerRan = false

createTerminus(server, {
healthChecks: {
'/health': () => {
onHealthCheckRan = true
const error = new Error()
error.statusCode = 500
return Promise.reject(error)
}
},
logger: () => {
loggerRan = true
}
})
server.listen(8000)

const res = await fetch('http://localhost:8000/health')
expect(res.status).to.eql(500)
expect(onHealthCheckRan).to.eql(true)
expect(loggerRan).to.eql(true)
})

it('returns custom status code on reject (prevailing over global custom status code)', async () => {
let onHealthCheckRan = false
let loggerRan = false

createTerminus(server, {
healthChecks: {
'/health': () => {
onHealthCheckRan = true
const error = new Error()
error.statusCode = 500
return Promise.reject(error)
}
},
logger: () => {
loggerRan = true
},
statusError: 501
})
server.listen(8000)

const res = await fetch('http://localhost:8000/health')
expect(res.status).to.eql(500)
expect(onHealthCheckRan).to.eql(true)
expect(loggerRan).to.eql(true)
})

it('exposes internal state (isShuttingDown: false) to health check', async () => {
let onHealthCheckRan = false
let exposedState
Expand Down
2 changes: 2 additions & 0 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ declare module "@godaddy/terminus" {
signal?: string;
signals?: string[];
sendFailuresDuringShutdown?: boolean;
statusOk?: number,
statusError?: number,
onSignal?: () => Promise<any>;
onSendFailureDuringShutdown?: () => Promise<any>;
onShutdown?: () => Promise<any>;
Expand Down

0 comments on commit ee94766

Please sign in to comment.