├── .editorconfig ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── package.json ├── src ├── delay.js └── index.js └── test ├── helpers ├── createHttpServer.js ├── createHttpsServer.js └── createTests.js └── src ├── HttpTerminator.js ├── http.js └── https.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*.js] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | max_line_length = 120 12 | 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | *.log 5 | .* 6 | !.nvmrc 7 | !.editorconfig 8 | !.eslintignore 9 | !.gitignore 10 | !.README 11 | /package-lock.json 12 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Gajus Kuizinas (http://gajus.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lil-http-terminator 🦾 2 | 3 | Gracefully terminates HTTP(S) server. 4 | 5 | This module was forked from the amazing [http-terminator](https://github.com/gajus/http-terminator). The important changes: 6 | 7 | - Zero dependencies, 11 KB on your disk. The original `http-terminator` brings in more than 20 sub-dependencies, >450 files, 2 MB total. 8 | - Removed TypeScript and a dozen of supporting files, configurations, etc. No more code transpilation. 9 | - Simpler API. Now you do `require("lil-http-terminator")({ server });` to get a terminator object. 10 | - The termination never throws. You don't want to handle unexpected exceptions during your server shutdown. 11 | - Termination won't hang forever if server never closes the port because some browsers disrespect `connection:close` header. 12 | 13 | ## Behaviour 14 | 15 | When you call [`server.close()`](https://nodejs.org/api/http.html#http_server_close_callback), it stops the server from accepting new connections, but it keeps the existing connections open indefinitely. This can result in your server hanging indefinitely due to keep-alive connections or because of the ongoing requests that do not produce a response. Therefore, in order to close the server, you must track creation of all connections and terminate them yourself. 16 | 17 | `lil-http-terminator` implements the logic for tracking all connections and their termination upon a timeout. `lil-http-terminator` also ensures graceful communication of the server intention to shutdown to any clients that are currently receiving response from this server. 18 | 19 | ## API 20 | 21 | ```js 22 | const HttpTerminator = require("lil-http-terminator"); 23 | 24 | const terminator = HttpTerminator({ 25 | server, // required. The node.js http server object instance 26 | 27 | // optional 28 | gracefulTerminationTimeout: 1000, // optional, how much time we give "keep-alive" connections to close before destryong them 29 | maxWaitTimeout: 30000, // optional, termination will return {success:false,code:"TIMED_OUT"} if it takes longer than that 30 | logger: console, // optional, default is `global.console`. If termination goes wild the module might log about it using `logger.warn()`. 31 | }); 32 | 33 | // Do not call server.close(); Instead call this: 34 | const { success, code, message, error } = await terminator.terminate(); 35 | if (!success) { 36 | if (code === "TIMED_OUT") console.log(message); 37 | if (code === "SERVER_ERROR") console.error(message, error); 38 | if (code === "INTERNAL_ERROR") console.error(message, error); 39 | } 40 | ``` 41 | 42 | ## Usage 43 | 44 | Use the terminator when node.js process is shutting down. 45 | 46 | ```js 47 | const http = require("http"); 48 | 49 | const server = http.createServer(); 50 | 51 | const httpTerminator = require("lil-http-terminator")({ server }); 52 | 53 | async function shutdown(signal) { 54 | console.log(`Received ${signal}. Shutting down.`) 55 | const { success, code, message, error } = await httpTerminator.terminate(); 56 | console.log(`HTTP server closure result: ${success} ${code} ${message} ${error || ""}`); 57 | process.exit(0); 58 | } 59 | 60 | process.on("SIGTERM", shutdown); // used by K8s, AWS ECS, etc. 61 | process.on("SIGINT", shutdown); // Atom, VSCode, WebStorm or Terminal Ctrl+C 62 | ``` 63 | 64 | ## Alternative libraries 65 | 66 | There are several alternative libraries that implement comparable functionality, e.g. 67 | 68 | - https://github.com/gajus/http-terminator (origin of this module) 69 | - https://github.com/hunterloftis/stoppable 70 | - https://github.com/thedillonb/http-shutdown 71 | - https://github.com/tellnes/http-close 72 | - https://github.com/sebhildebrandt/http-graceful-shutdown 73 | 74 | The main benefit of `lil-http-terminator` is that: 75 | 76 | - it does not have any dependencies 77 | - it never throws any errors but resolves an object: `{success:Boolean, code:String, message:String, error?:Error}`. 78 | - it never hangs if server can't be closed because of bad browser behaviour. Returns `{success:false,code:"TIMED_OUT"}`. 79 | - it does not monkey-patch Node.js API 80 | - it immediately destroys all sockets without an attached HTTP request 81 | - it allows graceful timeout to sockets with ongoing HTTP requests 82 | - it properly handles HTTPS connections 83 | - it informs connections using keep-alive that server is shutting down by setting a `connection: close` header 84 | - it does not terminate the Node.js process 85 | 86 | ## FAQ 87 | 88 | ### What is the use case for lil-http-terminator? 89 | 90 | To gracefully terminate a HTTP server. 91 | 92 | We say that a service is gracefully terminated when service stops accepting new clients, but allows time to complete the existing requests. 93 | 94 | There are several reasons to terminate services gracefully: 95 | 96 | - Terminating a service gracefully ensures that the client experience is not affected (assuming the service is load-balanced). 97 | - If your application is stateful, then when services are not terminated gracefully, you are risking data corruption. 98 | - Forcing termination of the service with a timeout ensures timely termination of the service (otherwise the service can remain hanging indefinitely). 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "ava": { 3 | "files": [ 4 | "test/src/**/*" 5 | ] 6 | }, 7 | "description": "Zero dependencies, gracefully terminates HTTP(S) server.", 8 | "devDependencies": { 9 | "agentkeepalive": "^4.2.1", 10 | "ava": "^4.0.1", 11 | "eslint": "^8.9.0", 12 | "got": "^11.8.3", 13 | "mocha": "^9.2.1", 14 | "nyc": "^15.1.0", 15 | "pem": "^1.14.6", 16 | "prettier": "^2.5.1" 17 | }, 18 | "eslintConfig": { 19 | "parserOptions": { 20 | "ecmaVersion": 2021 21 | }, 22 | "env": { 23 | "es6": true, 24 | "node": true, 25 | "mocha": true 26 | }, 27 | "extends": "eslint:recommended" 28 | }, 29 | "engines": { 30 | "node": ">=12" 31 | }, 32 | "files": [ 33 | "src" 34 | ], 35 | "keywords": [ 36 | "docker", 37 | "kubernetes", 38 | "prometheus", 39 | "http", 40 | "https", 41 | "keep-alive", 42 | "close", 43 | "terminate" 44 | ], 45 | "license": "BSD-3-Clause", 46 | "source": "src/index.js", 47 | "main": "src/index.js", 48 | "name": "lil-http-terminator", 49 | "repository": { 50 | "type": "git", 51 | "url": "https://github.com/flash-oss/lil-http-terminator" 52 | }, 53 | "scripts": { 54 | "ci": "npm i && npm t && npm run lint", 55 | "test": "NODE_ENV=test mocha --recursive --exit", 56 | "cov": "NODE_ENV=test nyc --reporter=lcov --reporter=text-summary mocha --recursive --exit", 57 | "lint": "eslint ./src ./test" 58 | }, 59 | "version": "1.2.3" 60 | } 61 | -------------------------------------------------------------------------------- /src/delay.js: -------------------------------------------------------------------------------- 1 | module.exports = function delay(time) { 2 | return new Promise((resolve) => setTimeout(() => resolve(), time).unref()); 3 | }; 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const http = require("http"); 3 | const delay = require("./delay"); 4 | 5 | /** 6 | * @param server {http.Server} 7 | * @param [gracefulTerminationTimeout=1000] How long should we wait before destroying some hung sockets 8 | * @param [maxWaitTimeout=30000] How much time you give the HTTP server to be terminated. 9 | * @param [logger=console] Prints warnings if something goes non standard way. 10 | * @return {{terminate(): Promise<{code: string, success: boolean, message: string, [error]: Error}>}} 11 | */ 12 | module.exports = function HttpTerminator({ 13 | server, 14 | gracefulTerminationTimeout = 1000, 15 | maxWaitTimeout = 30000, 16 | logger = console, 17 | } = {}) { 18 | assert(server); 19 | 20 | const _sockets = new Set(); 21 | const _secureSockets = new Set(); 22 | 23 | let terminating; 24 | 25 | server.on("connection", (socket) => { 26 | if (terminating) { 27 | socket.destroy(); 28 | } else { 29 | _sockets.add(socket); 30 | 31 | socket.once("close", () => { 32 | _sockets.delete(socket); 33 | }); 34 | } 35 | }); 36 | 37 | server.on("secureConnection", (socket) => { 38 | if (terminating) { 39 | socket.destroy(); 40 | } else { 41 | _secureSockets.add(socket); 42 | 43 | socket.once("close", () => { 44 | _secureSockets.delete(socket); 45 | }); 46 | } 47 | }); 48 | 49 | /** 50 | * Evaluate whether additional steps are required to destroy the socket. 51 | * 52 | * @see https://github.com/nodejs/node/blob/57bd715d527aba8dae56b975056961b0e429e91e/lib/_http_client.js#L363-L413 53 | */ 54 | function destroySocket(socket) { 55 | socket.destroy(); 56 | 57 | if (socket.server instanceof http.Server) { 58 | _sockets.delete(socket); 59 | } else { 60 | _secureSockets.delete(socket); 61 | } 62 | } 63 | 64 | return { 65 | _secureSockets, 66 | _sockets, 67 | /** 68 | * Terminates the given server. 69 | * @return {Promise<{code: string, success: boolean, message: string, [error]: Error}>} 70 | */ 71 | async terminate() { 72 | try { 73 | if (terminating) { 74 | logger.warn("lil-http-terminator: already terminating HTTP server"); 75 | 76 | return terminating; 77 | } 78 | 79 | // This is a built-in method available starting Node 16. Can speeds up things by a few seconds. 80 | if (server.closeIdleConnections) server.closeIdleConnections(); 81 | 82 | let resolveTerminating; 83 | 84 | terminating = Promise.race([ 85 | new Promise((resolve) => { 86 | resolveTerminating = resolve; 87 | }), 88 | delay(maxWaitTimeout).then(() => ({ 89 | success: false, 90 | code: "TIMED_OUT", 91 | message: `Server didn't close in ${maxWaitTimeout} msec. Use server.closeAllConnections()`, 92 | })), 93 | ]); 94 | 95 | server.on("request", (incomingMessage, outgoingMessage) => { 96 | if (!outgoingMessage.headersSent) { 97 | outgoingMessage.setHeader("connection", "close"); 98 | } 99 | }); 100 | 101 | for (const socket of _sockets) { 102 | // This is the HTTP CONNECT request socket. 103 | if (!(socket.server instanceof http.Server)) { 104 | continue; 105 | } 106 | 107 | const serverResponse = socket._httpMessage; 108 | 109 | if (serverResponse) { 110 | if (!serverResponse.headersSent) { 111 | serverResponse.setHeader("connection", "close"); 112 | } 113 | 114 | continue; 115 | } 116 | 117 | destroySocket(socket); 118 | } 119 | 120 | for (const socket of _secureSockets) { 121 | const serverResponse = socket._httpMessage; 122 | 123 | if (serverResponse) { 124 | if (!serverResponse.headersSent) { 125 | serverResponse.setHeader("connection", "close"); 126 | } 127 | 128 | continue; 129 | } 130 | 131 | destroySocket(socket); 132 | } 133 | 134 | if (_sockets.size || _secureSockets.size) { 135 | const endWaitAt = Date.now() + gracefulTerminationTimeout; 136 | while ((_sockets.size || _secureSockets.size) && Date.now() < endWaitAt) await delay(1); 137 | 138 | for (const socket of _sockets) { 139 | destroySocket(socket); 140 | } 141 | 142 | for (const socket of _secureSockets) { 143 | destroySocket(socket); 144 | } 145 | } 146 | 147 | server.close((error) => { 148 | if (error) { 149 | logger.warn("lil-http-terminator: server error while closing", error); 150 | resolveTerminating({ success: false, code: "SERVER_ERROR", message: error.message, error }); 151 | } else { 152 | resolveTerminating({ 153 | success: true, 154 | code: "TERMINATED", 155 | message: "Server successfully closed", 156 | }); 157 | } 158 | }); 159 | 160 | return terminating; 161 | } catch (error) { 162 | logger.warn("lil-http-terminator: internal error", error); 163 | return { success: false, code: "INTERNAL_ERROR", message: error.message, error }; 164 | } 165 | }, 166 | }; 167 | }; 168 | -------------------------------------------------------------------------------- /test/helpers/createHttpServer.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require("http"); 2 | const { promisify } = require("util"); 3 | 4 | module.exports = (requestHandler) => { 5 | const server = createServer(requestHandler); 6 | 7 | let serverShuttingDown; 8 | 9 | const stop = () => { 10 | if (serverShuttingDown) { 11 | return serverShuttingDown; 12 | } 13 | 14 | serverShuttingDown = promisify(server.close.bind(server))(); 15 | 16 | return serverShuttingDown; 17 | }; 18 | 19 | const getConnections = () => { 20 | return promisify(server.getConnections.bind(server))(); 21 | }; 22 | 23 | return new Promise((resolve, reject) => { 24 | server.once("error", reject); 25 | 26 | server.listen(() => { 27 | const port = server.address().port; 28 | const url = "http://localhost:" + port; 29 | 30 | resolve({ 31 | getConnections, 32 | port, 33 | server, 34 | stop, 35 | url, 36 | }); 37 | }); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /test/helpers/createHttpsServer.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require("https"); 2 | const { promisify } = require("util"); 3 | const pem = require("pem"); 4 | 5 | module.exports = async (requestHandler) => { 6 | const { serviceKey, certificate, csr } = await promisify(pem.createCertificate)({ 7 | days: 365, 8 | selfSigned: true, 9 | }); 10 | 11 | const httpsOptions = { 12 | ca: csr, 13 | cert: certificate, 14 | key: serviceKey, 15 | }; 16 | 17 | const server = createServer(httpsOptions, requestHandler); 18 | 19 | let serverShutingDown; 20 | 21 | const stop = () => { 22 | if (serverShutingDown) { 23 | return serverShutingDown; 24 | } 25 | 26 | serverShutingDown = promisify(server.close.bind(server))(); 27 | 28 | return serverShutingDown; 29 | }; 30 | 31 | const getConnections = () => { 32 | return promisify(server.getConnections.bind(server))(); 33 | }; 34 | 35 | return new Promise((resolve, reject) => { 36 | server.once("error", reject); 37 | 38 | server.listen(() => { 39 | const port = server.address().port; 40 | const url = "https://localhost:" + port; 41 | 42 | resolve({ 43 | getConnections, 44 | port, 45 | server, 46 | stop, 47 | url, 48 | }); 49 | }); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /test/helpers/createTests.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const KeepAliveHttpAgent = require("agentkeepalive"); 3 | const delay = require("../../src/delay"); 4 | const safeGot = require("got"); 5 | const createHttpTerminator = require("../../src"); 6 | 7 | const got = safeGot.extend({ 8 | https: { 9 | rejectUnauthorized: false 10 | } 11 | }); 12 | 13 | const KeepAliveHttpsAgent = KeepAliveHttpAgent.HttpsAgent; 14 | 15 | module.exports = function(createHttpServer) { 16 | it("terminates HTTP server with no connections", async function() { 17 | const httpServer = await createHttpServer(() => {}); 18 | 19 | this.timeout(1000); 20 | 21 | assert.strictEqual(true, httpServer.server.listening); 22 | 23 | const terminator = createHttpTerminator({ 24 | server: httpServer.server 25 | }); 26 | 27 | await terminator.terminate(); 28 | 29 | assert.notStrictEqual(httpServer.server.listening, true); 30 | }); 31 | 32 | it("terminates hanging sockets after gracefulTerminationTimeout", async function() { 33 | let serverCreated = false; 34 | 35 | const httpServer = await createHttpServer(() => { 36 | serverCreated = true; 37 | }); 38 | 39 | this.timeout(500); 40 | 41 | const terminator = createHttpTerminator({ 42 | gracefulTerminationTimeout: 150, 43 | server: httpServer.server 44 | }); 45 | 46 | got(httpServer.url); 47 | 48 | await delay(50); 49 | 50 | assert.strictEqual(true, serverCreated); 51 | 52 | terminator.terminate(); 53 | 54 | await delay(100); 55 | 56 | // The timeout has not passed. 57 | assert.strictEqual(await httpServer.getConnections(), 1); 58 | 59 | await delay(100); 60 | 61 | assert.strictEqual(await httpServer.getConnections(), 0); 62 | }); 63 | 64 | it("server stops accepting new connections after terminator.terminate() is called", async function() { 65 | let callCount = 0; 66 | 67 | function requestHandler(incomingMessage, outgoingMessage) { 68 | if (callCount === 0) { 69 | setTimeout(() => { 70 | outgoingMessage.end("foo"); 71 | }, 100); 72 | } else if (callCount === 1) { 73 | outgoingMessage.end("bar"); 74 | } 75 | callCount += 1; 76 | } 77 | 78 | const httpServer = await createHttpServer(requestHandler); 79 | 80 | this.timeout(500); 81 | 82 | const terminator = createHttpTerminator({ 83 | gracefulTerminationTimeout: 150, 84 | server: httpServer.server 85 | }); 86 | 87 | const request0 = got(httpServer.url); 88 | 89 | await delay(50); 90 | 91 | terminator.terminate(); 92 | 93 | await delay(50); 94 | 95 | const request1 = got(httpServer.url, { 96 | retry: 0, 97 | timeout: { 98 | connect: 50 99 | } 100 | }); 101 | 102 | // @todo https://stackoverflow.com/q/59832897/368691 103 | await assert.rejects(request1); 104 | 105 | const response0 = await request0; 106 | 107 | assert.strictEqual(response0.headers.connection, "close"); 108 | assert.strictEqual(response0.body, "foo"); 109 | }); 110 | 111 | it("ongoing requests receive {connection: close} header", async function() { 112 | const httpServer = await createHttpServer((incomingMessage, outgoingMessage) => { 113 | setTimeout(() => { 114 | outgoingMessage.end("foo"); 115 | }, 100); 116 | }); 117 | 118 | this.timeout(600); 119 | 120 | const terminator = createHttpTerminator({ 121 | gracefulTerminationTimeout: 150, 122 | server: httpServer.server 123 | }); 124 | 125 | const httpAgent = new KeepAliveHttpAgent({ 126 | maxSockets: 1 127 | }); 128 | 129 | const httpsAgent = new KeepAliveHttpsAgent({ 130 | maxSockets: 1 131 | }); 132 | 133 | const request = got(httpServer.url, { 134 | agent: { 135 | http: httpAgent, 136 | https: httpsAgent 137 | } 138 | }); 139 | 140 | await delay(50); 141 | 142 | terminator.terminate(); 143 | 144 | const response = await request; 145 | 146 | assert.strictEqual(response.headers.connection, "close"); 147 | assert.strictEqual(response.body, "foo"); 148 | }); 149 | 150 | it("ongoing requests receive {connection: close} header (new request reusing an existing socket)", async function() { 151 | let callCount = 0; 152 | 153 | function requestHandler(incomingMessage, outgoingMessage) { 154 | if (callCount === 0) { 155 | outgoingMessage.write("foo"); 156 | 157 | setTimeout(() => { 158 | outgoingMessage.end("bar"); 159 | }, 51); 160 | } else if (callCount === 1) { 161 | // @todo Unable to intercept the response without the delay. 162 | // When `end()` is called immediately, the `request` event 163 | // already has `headersSent=true`. It is unclear how to intercept 164 | // the response beforehand. 165 | setTimeout(() => { 166 | outgoingMessage.end("baz"); 167 | }, 51); 168 | } 169 | callCount += 1; 170 | } 171 | 172 | const httpServer = await createHttpServer(requestHandler); 173 | 174 | this.timeout(1000); 175 | 176 | const terminator = createHttpTerminator({ 177 | gracefulTerminationTimeout: 150, 178 | server: httpServer.server 179 | }); 180 | 181 | const httpAgent = new KeepAliveHttpAgent({ 182 | maxSockets: 1 183 | }); 184 | 185 | const httpsAgent = new KeepAliveHttpsAgent({ 186 | maxSockets: 1 187 | }); 188 | 189 | const request0 = got(httpServer.url, { 190 | agent: { 191 | http: httpAgent, 192 | https: httpsAgent 193 | } 194 | }); 195 | 196 | await delay(50); 197 | 198 | terminator.terminate(); 199 | 200 | const request1 = got(httpServer.url, { 201 | agent: { 202 | http: httpAgent, 203 | https: httpsAgent 204 | }, 205 | retry: 0 206 | }); 207 | 208 | await delay(75); 209 | 210 | assert.strictEqual(callCount, 2); 211 | 212 | const response0 = await request0; 213 | 214 | assert.strictEqual(response0.headers.connection, "keep-alive"); 215 | assert.strictEqual(response0.body, "foobar"); 216 | 217 | const response1 = await request1; 218 | 219 | assert.strictEqual(response1.headers.connection, "close"); 220 | assert.strictEqual(response1.body, "baz"); 221 | }); 222 | 223 | it("does not send {connection: close} when server is not terminating", async function() { 224 | const httpServer = await createHttpServer((incomingMessage, outgoingMessage) => { 225 | setTimeout(() => { 226 | outgoingMessage.end("foo"); 227 | }, 50); 228 | }); 229 | 230 | this.timeout(1000); 231 | 232 | createHttpTerminator({ 233 | server: httpServer.server 234 | }); 235 | 236 | const httpAgent = new KeepAliveHttpAgent({ 237 | maxSockets: 1 238 | }); 239 | 240 | const httpsAgent = new KeepAliveHttpsAgent({ 241 | maxSockets: 1 242 | }); 243 | 244 | const response = await got(httpServer.url, { 245 | agent: { 246 | http: httpAgent, 247 | https: httpsAgent 248 | } 249 | }); 250 | 251 | assert.strictEqual(response.headers.connection, "keep-alive"); 252 | }); 253 | }; 254 | -------------------------------------------------------------------------------- /test/src/HttpTerminator.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const KeepAliveHttpAgent = require("agentkeepalive"); 3 | const delay = require("../../src/delay"); 4 | const safeGot = require("got"); 5 | const HttpTerminator = require("../../src"); 6 | const createHttpServer = require("../helpers/createHttpServer"); 7 | const createHttpsServer = require("../helpers/createHttpsServer"); 8 | 9 | const got = safeGot.extend({ 10 | https: { 11 | rejectUnauthorized: false 12 | } 13 | }); 14 | 15 | describe("lil-http-terminator", function() { 16 | it("terminates HTTP server with no connections", async function() { 17 | this.timeout(100); 18 | 19 | const httpServer = await createHttpServer(() => {}); 20 | 21 | assert.strictEqual(httpServer.server.listening, true); 22 | 23 | const terminator = HttpTerminator({ 24 | server: httpServer.server 25 | }); 26 | 27 | const result = await terminator.terminate(); 28 | 29 | assert.notStrictEqual(httpServer.server.listening, true); 30 | assert.strictEqual(result.success, true); 31 | assert.strictEqual(result.code, "TERMINATED"); 32 | }); 33 | 34 | it("terminates hanging sockets after httpResponseTimeout", async function() { 35 | this.timeout(500); 36 | 37 | let serverCreated = false; 38 | 39 | const httpServer = await createHttpServer(() => { 40 | serverCreated = true; 41 | }); 42 | 43 | const terminator = HttpTerminator({ 44 | gracefulTerminationTimeout: 150, 45 | server: httpServer.server 46 | }); 47 | 48 | got(httpServer.url); 49 | 50 | await delay(50); 51 | 52 | assert.strictEqual(serverCreated, true); 53 | 54 | const terminationPromise = terminator.terminate(); 55 | 56 | await delay(100); 57 | 58 | // The timeout has not passed. 59 | assert.strictEqual(await httpServer.getConnections(), 1); 60 | 61 | await delay(100); 62 | 63 | assert.strictEqual(await httpServer.getConnections(), 0); 64 | 65 | const result = await terminationPromise; 66 | 67 | assert.strictEqual(result.success, true); 68 | }); 69 | 70 | it("server stops accepting new connections after terminator.terminate() is called", async function() { 71 | this.timeout(500); 72 | 73 | const httpServer = await createHttpServer((incomingMessage, outgoingMessage) => { 74 | setTimeout(() => { 75 | outgoingMessage.end("foo"); 76 | }, 100); 77 | }); 78 | 79 | const terminator = HttpTerminator({ 80 | gracefulTerminationTimeout: 150, 81 | server: httpServer.server 82 | }); 83 | 84 | const request0 = got(httpServer.url); 85 | 86 | await delay(50); 87 | 88 | const terminationPromise = terminator.terminate(); 89 | 90 | await delay(50); 91 | 92 | const request1 = got(httpServer.url, { 93 | retry: 0, 94 | timeout: { 95 | connect: 50 96 | } 97 | }); 98 | 99 | await assert.rejects(request1); 100 | 101 | const response0 = await request0; 102 | 103 | assert.strictEqual(response0.headers.connection, "close"); 104 | assert.strictEqual(response0.body, "foo"); 105 | 106 | const result = await terminationPromise; 107 | 108 | assert.strictEqual(result.success, true); 109 | }); 110 | 111 | it("ongoing requests receive {connection: close} header", async function() { 112 | this.timeout(500); 113 | 114 | const httpServer = await createHttpServer((incomingMessage, outgoingMessage) => { 115 | setTimeout(() => { 116 | outgoingMessage.end("foo"); 117 | }, 100); 118 | }); 119 | 120 | const terminator = HttpTerminator({ 121 | gracefulTerminationTimeout: 150, 122 | server: httpServer.server 123 | }); 124 | 125 | const request = got(httpServer.url, { 126 | agent: { 127 | http: new KeepAliveHttpAgent() 128 | } 129 | }); 130 | 131 | await delay(50); 132 | 133 | const terminationPromise = terminator.terminate(); 134 | 135 | const response = await request; 136 | 137 | assert.strictEqual(response.headers.connection, "close"); 138 | assert.strictEqual(response.body, "foo"); 139 | 140 | const result = await terminationPromise; 141 | 142 | assert.strictEqual(result.success, true); 143 | }); 144 | 145 | it("ongoing requests receive {connection: close} header (new request reusing an existing socket)", async function() { 146 | this.timeout(1000); 147 | 148 | let callCount = 0; 149 | 150 | function requestHandler(incomingMessage, outgoingMessage) { 151 | if (callCount === 0) { 152 | outgoingMessage.write("foo"); 153 | 154 | setTimeout(() => { 155 | outgoingMessage.end("bar"); 156 | }, 51); 157 | } else if (callCount === 1) { 158 | // @todo Unable to intercept the response without the delay. 159 | // When `end()` is called immediately, the `request` event 160 | // already has `headersSent=true`. It is unclear how to intercept 161 | // the response beforehand. 162 | setTimeout(() => { 163 | outgoingMessage.end("baz"); 164 | }, 51); 165 | } 166 | callCount += 1; 167 | } 168 | 169 | const httpServer = await createHttpServer(requestHandler); 170 | 171 | const terminator = HttpTerminator({ 172 | gracefulTerminationTimeout: 150, 173 | server: httpServer.server 174 | }); 175 | 176 | const agent = new KeepAliveHttpAgent({ 177 | maxSockets: 1 178 | }); 179 | 180 | const request0 = got(httpServer.url, { 181 | agent: { 182 | http: agent 183 | } 184 | }); 185 | 186 | await delay(50); 187 | 188 | const terminationPromise = terminator.terminate(); 189 | 190 | const request1 = got(httpServer.url, { 191 | agent: { 192 | http: agent 193 | }, 194 | retry: 0 195 | }); 196 | 197 | await delay(75); 198 | 199 | assert.strictEqual(callCount, 2); 200 | 201 | const response0 = await request0; 202 | 203 | assert.strictEqual(response0.headers.connection, "keep-alive"); 204 | assert.strictEqual(response0.body, "foobar"); 205 | 206 | const response1 = await request1; 207 | 208 | assert.strictEqual(response1.headers.connection, "close"); 209 | assert.strictEqual(response1.body, "baz"); 210 | 211 | const result = await terminationPromise; 212 | 213 | assert.strictEqual(result.success, true); 214 | }); 215 | 216 | it("empties internal socket collection", async function() { 217 | this.timeout(500); 218 | 219 | const httpServer = await createHttpServer(function(incomingMessage, outgoingMessage) { 220 | outgoingMessage.end("foo"); 221 | }); 222 | 223 | const terminator = HttpTerminator({ 224 | gracefulTerminationTimeout: 150, 225 | server: httpServer.server 226 | }); 227 | 228 | await got(httpServer.url); 229 | 230 | await delay(50); 231 | 232 | assert.strictEqual(terminator._sockets.size, 0); 233 | assert.strictEqual(terminator._secureSockets.size, 0); 234 | 235 | const result = await terminator.terminate(); 236 | 237 | assert.strictEqual(result.success, true); 238 | }); 239 | 240 | it("empties internal socket collection for https server", async function() { 241 | this.timeout(500); 242 | 243 | const httpsServer = await createHttpsServer((incomingMessage, outgoingMessage) => { 244 | outgoingMessage.end("foo"); 245 | }); 246 | 247 | const terminator = HttpTerminator({ 248 | gracefulTerminationTimeout: 150, 249 | server: httpsServer.server 250 | }); 251 | 252 | await got(httpsServer.url); 253 | 254 | await delay(50); 255 | 256 | assert.strictEqual(terminator._secureSockets.size, 0); 257 | 258 | const result = await terminator.terminate(); 259 | 260 | assert.strictEqual(result.success, true); 261 | }); 262 | 263 | it("returns {success: false, code: 'TIMED_OUT'} if server couldn't close in time", async function() { 264 | this.timeout(500); 265 | 266 | const terminator = HttpTerminator({ 267 | gracefulTerminationTimeout: 100, 268 | maxWaitTimeout: 300, 269 | server: { 270 | on: () => {}, 271 | close: cb => setTimeout(cb, 400) 272 | } 273 | }); 274 | 275 | const result = await terminator.terminate(); 276 | 277 | assert.notStrictEqual(result.success, true); 278 | assert.strictEqual(result.code, "TIMED_OUT"); 279 | }); 280 | 281 | it("returns {success: false, code: 'SERVER_ERROR'} if server closing gives error", async function() { 282 | this.timeout(500); 283 | 284 | const terminator = HttpTerminator({ 285 | gracefulTerminationTimeout: 10, 286 | logger: { warn: () => {} }, 287 | server: { 288 | on: () => {}, 289 | close: cb => setTimeout(() => cb(new Error("Can't close socket for some reason")), 400) 290 | } 291 | }); 292 | 293 | const result = await terminator.terminate(); 294 | 295 | assert.notStrictEqual(result.success, true); 296 | assert.strictEqual(result.code, "SERVER_ERROR"); 297 | }); 298 | 299 | it("returns {success: false, code: 'INTERNAL_ERROR'} if unexpected exception", async function() { 300 | this.timeout(500); 301 | 302 | const terminator = HttpTerminator({ 303 | gracefulTerminationTimeout: 10, 304 | logger: { warn: () => {} }, 305 | server: { 306 | on: () => {}, 307 | close: () => { 308 | throw new Error("Unexpected"); 309 | } 310 | } 311 | }); 312 | 313 | const result = await terminator.terminate(); 314 | 315 | assert.notStrictEqual(result.success, true); 316 | assert.strictEqual(result.code, "INTERNAL_ERROR"); 317 | }); 318 | 319 | it("closes immediately after in-flight connections are closed", async function() { 320 | this.timeout(1000); 321 | 322 | function requestHandler(incomingMessage, outgoingMessage) { 323 | setTimeout(() => { 324 | outgoingMessage.end("foo"); 325 | }, 100); 326 | } 327 | 328 | const httpServer = await createHttpServer(requestHandler); 329 | 330 | assert.strictEqual(httpServer.server.listening, true); 331 | 332 | const terminator = HttpTerminator({ 333 | gracefulTerminationTimeout: 500, 334 | server: httpServer.server 335 | }); 336 | 337 | got(httpServer.url); 338 | 339 | await delay(50); 340 | 341 | assert.strictEqual(await httpServer.getConnections(), 1); 342 | 343 | terminator.terminate(); 344 | 345 | // Wait for outgoingMessage.end to be called, plus a few extra ms for the 346 | // terminator to finish polling in-flight connections. (Do not, however, wait 347 | // long enough to trigger graceful termination.) 348 | await delay(75); 349 | 350 | assert.strictEqual(await httpServer.getConnections(), 0); 351 | }); 352 | }); 353 | -------------------------------------------------------------------------------- /test/src/http.js: -------------------------------------------------------------------------------- 1 | const createHttpServer = require("../helpers/createHttpServer"); 2 | const createTests = require("../helpers/createTests"); 3 | 4 | describe("http", () => { 5 | createTests(createHttpServer); 6 | }); 7 | -------------------------------------------------------------------------------- /test/src/https.js: -------------------------------------------------------------------------------- 1 | const createHttpsServer = require("../helpers/createHttpsServer"); 2 | const createTests = require("../helpers/createTests"); 3 | 4 | describe("https", () => { 5 | createTests(createHttpsServer); 6 | }); 7 | --------------------------------------------------------------------------------