http-graceful-shutdown
Version:
gracefully shuts downs http server
313 lines (268 loc) • 8.19 kB
JavaScript
// =============================================================================
// _
// |_ _|_ _|_ ._ __ _ ._ _. _ _ _|_ | __ _ |_ _|_ _| _ ._
// | | |_ |_ |_) (_| | (_| (_ (/_ | |_| | _> | | |_| |_ (_| (_) \/\/ | |
// | _|
// -----------------------------------------------------------------------------
// gracefully shuts downs http server
// can be used with http, express, koa, ...
// (c) 2026 Sebastian Hildebrandt
// License: MIT
// =============================================================================
const debug = require("debug")("http-graceful-shutdown");
const http = require("node:http");
/**
* Gracefully shuts down `server` when the process receives
* the passed signals
*
* @param {http.Server} server
* @param {object} opts
* signals: string (each signal separated by SPACE)
* timeout: timeout value for forceful shutdown in ms
* forceExit: force process.exit() - otherwise just let event loop clear
* development: boolean value (if true, no graceful shutdown to speed up development
* preShutdown: optional function. Needs to return a promise. - HTTP sockets are still available and untouched
* onShutdown: optional function. Needs to return a promise.
* finally: optional function, handled at the end of the shutdown.
*/
function GracefulShutdown(server, opts) {
// option handling
// ----------------------------------
opts = opts || {};
// merge opts with default options
const options = Object.assign(
{
signals: "SIGINT SIGTERM",
timeout: 30000,
development: false,
forceExit: true,
onShutdown: (signal) => Promise.resolve(signal),
preShutdown: (signal) => Promise.resolve(signal),
},
opts,
);
let isShuttingDown = false;
const connections = {};
let connectionCounter = 0;
const secureConnections = {};
let secureConnectionCounter = 0;
let failed = false;
let finalRun = false;
function onceFactory() {
let called = false;
return (emitter, events, callback) => {
function call() {
if (!called) {
called = true;
return callback.apply(this, arguments);
}
}
events.forEach((e) => emitter.on(e, call));
};
}
const signals = options.signals
.split(" ")
.map((s) => s.trim())
.filter((s) => !!s.length);
const once = onceFactory();
once(process, signals, (signal) => {
debug("received shut down signal", signal);
shutdown(signal)
.then(() => {
if (options.forceExit) {
process.exit(failed ? 1 : 0);
}
})
.catch((err) => {
debug("server shut down error occurred", err);
process.exit(1);
});
});
// helper function
// ----------------------------------
function isFunction(functionToCheck) {
const getType = Object.prototype.toString.call(functionToCheck);
return /^\[object\s([a-zA-Z]+)?Function\]$/.test(getType);
}
function destroy(socket, force = false) {
if ((socket._isIdle && isShuttingDown) || force) {
socket.destroy();
if (socket.server instanceof http.Server) {
delete connections[socket._connectionId];
} else {
delete secureConnections[socket._connectionId];
}
}
}
function destroyAllConnections(force = false) {
// destroy empty and idle connections / all connections (if force = true)
debug(`Destroy Connections : ${force ? "forced close" : "close"}`);
let counter = 0;
let secureCounter = 0;
Object.keys(connections).forEach((key) => {
const socket = connections[key];
const serverResponse = socket._httpMessage;
// send connection close header to open connections
if (serverResponse && !force) {
if (!serverResponse.headersSent) {
serverResponse.setHeader("connection", "close");
}
} else {
counter++;
destroy(socket);
}
});
debug(`Connections destroyed : ${counter}`);
debug(`Connection Counter : ${connectionCounter}`);
Object.keys(secureConnections).forEach((key) => {
const socket = secureConnections[key];
const serverResponse = socket._httpMessage;
// send connection close header to open connections
if (serverResponse && !force) {
if (!serverResponse.headersSent) {
serverResponse.setHeader("connection", "close");
}
} else {
secureCounter++;
destroy(socket);
}
});
debug(`Secure Connections destroyed : ${secureCounter}`);
debug(`Secure Connection Counter : ${secureConnectionCounter}`);
}
// set up server/process events
// ----------------------------------
server.on("request", (req, res) => {
req.socket._isIdle = false;
if (isShuttingDown && !res.headersSent) {
res.setHeader("connection", "close");
}
res.on("finish", () => {
req.socket._isIdle = true;
destroy(req.socket);
});
});
server.on("connection", (socket) => {
if (isShuttingDown) {
socket.destroy();
} else {
const id = connectionCounter++;
socket._isIdle = true;
socket._connectionId = id;
connections[id] = socket;
socket.once("close", () => {
delete connections[socket._connectionId];
});
}
});
server.on("secureConnection", (socket) => {
if (isShuttingDown) {
socket.destroy();
} else {
const id = secureConnectionCounter++;
socket._isIdle = true;
socket._connectionId = id;
secureConnections[id] = socket;
socket.once("close", () => {
delete secureConnections[socket._connectionId];
});
}
});
process.on("close", () => {
debug("closed");
});
// shutdown event (per signal)
// ----------------------------------
function shutdown(sig) {
function cleanupHttp() {
destroyAllConnections();
debug("Close http server");
return new Promise((resolve, reject) => {
server.close((err) => {
if (err) {
return reject(err);
}
return resolve(true);
});
});
}
debug(`shutdown signal - ${sig}`);
// Don't bother with graceful shutdown on development to speed up round trip
if (options.development) {
debug("DEV-Mode - immediate forceful shutdown");
return process.exit(0);
}
function finalHandler() {
if (!finalRun) {
finalRun = true;
if (options.finally && isFunction(options.finally)) {
debug("executing finally()");
options.finally();
}
}
return Promise.resolve();
}
// returns true if should force shut down. returns false for shut down without force
function waitForReadyToShutDown(totalNumInterval) {
debug(`waitForReadyToShutDown... ${totalNumInterval}`);
if (totalNumInterval === 0) {
// timeout reached
debug(
`Could not close connections in time (${options.timeout}ms), will forcefully shut down`,
);
return Promise.resolve(true);
}
// test all connections closed already?
const allConnectionsClosed =
Object.keys(connections).length === 0 &&
Object.keys(secureConnections).length === 0;
if (allConnectionsClosed) {
debug("All connections closed. Continue to shutting down");
return Promise.resolve(false);
}
debug("Schedule the next waitForReadyToShutdown");
return new Promise((resolve) => {
setTimeout(() => {
resolve(waitForReadyToShutDown(totalNumInterval - 1));
}, 250);
});
}
if (isShuttingDown) {
return Promise.resolve();
}
debug("shutting down");
return options
.preShutdown(sig)
.then(() => {
isShuttingDown = true;
return cleanupHttp();
})
.then(() => {
const pollIterations = options.timeout
? Math.round(options.timeout / 250)
: 0;
return waitForReadyToShutDown(pollIterations);
})
.then((force) => {
debug("Do onShutdown now");
// if after waiting for connections to drain within timeout period
// or if timeout has reached, we forcefully disconnect all sockets
if (force) {
destroyAllConnections(force);
}
return options.onShutdown(sig);
})
.then(finalHandler)
.catch((err) => {
const errString = typeof err === "string" ? err : JSON.stringify(err);
debug(errString);
failed = true;
throw errString;
});
}
function shutdownManual() {
return shutdown("manual");
}
return shutdownManual;
}
module.exports = GracefulShutdown;