Skip to content

Commit

Permalink
feat(status): custom status codes
Browse files Browse the repository at this point in the history
  • Loading branch information
gergelyke committed Jun 2, 2021
1 parent 04191cc commit 522df64
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 86 deletions.
80 changes: 44 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,24 @@ npm i @godaddy/terminus --save
const http = require('http');
const { createTerminus } = require('@godaddy/terminus');

function onSignal () {
function onSignal() {
console.log('server is starting cleanup');
return Promise.all([
// your clean logic, like closing database connections
]);
}

function onShutdown () {
function onShutdown() {
console.log('cleanup finished, server is shutting down');
}

function healthCheck ({ state }) {
function healthCheck({ state }) {
// `state.isShuttingDown` (boolean) shows whether the server is shutting down or not
return Promise.resolve(
return Promise
.resolve
// optionally include a resolve value to be included as
// info in the health check response
)
();
}

const server = http.createServer((request, response) => {
Expand All @@ -45,30 +46,33 @@ const server = http.createServer((request, response) => {
<h1>Hello, World!</h1>
</body>
</html>`
);
})
);
});

const options = {
// health check options
healthChecks: {
'/healthcheck': healthCheck, // a function accepting a state and returning a promise indicating service health,
'/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,
__unsafeExposeStackTraces: true // [optional = false] return stack traces in error response if healthchecks throw errors
__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)

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 All @@ -85,20 +89,24 @@ const { createTerminus, HealthCheckError } = require('@godaddy/terminus');
createTerminus(server, {
healthChecks: {
'/healthcheck': async function () {
const errors = []
return Promise.all([
// all your health checks goes here
].map(p => p.catch((error) => {
// silently collecting all the errors
errors.push(error)
return undefined
}))).then(() => {
const errors = [];
return Promise.all(
[
// all your health checks goes here
].map((p) =>
p.catch((error) => {
// silently collecting all the errors
errors.push(error);
return undefined;
})
)
).then(() => {
if (errors.length) {
throw new HealthCheckError('healthcheck failed', errors)
throw new HealthCheckError('healthcheck failed', errors);
}
})
}
}
});
},
},
});
```

Expand Down Expand Up @@ -155,17 +163,17 @@ During this time, it is possible that load-balancers (like the nginx ingress con
To make sure you don't lose any connections, we recommend delaying the shutdown with the number of milliseconds that's defined by the readiness probe in your deployment configuration. To help with this, terminus exposes an option called `beforeShutdown` that takes any Promise-returning function.

```javascript
function beforeShutdown () {
function beforeShutdown() {
// given your readiness probes run every 5 second
// may be worth using a bigger number so you won't
// run into any race conditions
return new Promise(resolve => {
setTimeout(resolve, 5000)
})
return new Promise((resolve) => {
setTimeout(resolve, 5000);
});
}
createTerminus(server, {
beforeShutdown
})
beforeShutdown,
});
```

[Learn more](https://github.com/kubernetes/contrib/issues/1140#issuecomment-231641402)
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
25 changes: 25 additions & 0 deletions lib/terminus.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,31 @@ describe('Terminus', () => {
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('exposes internal state (isShuttingDown: false) to health check', async () => {
let onHealthCheckRan = false
let exposedState
Expand Down

0 comments on commit 522df64

Please sign in to comment.