diff --git a/doc/ws.md b/doc/ws.md index 04c785feb..7d22a0480 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -80,6 +80,9 @@ This class represents a WebSocket server. It extends the `EventEmitter`. in response to a ping. Defaults to `true`. - `backlog` {Number} The maximum length of the queue of pending connections. - `clientTracking` {Boolean} Specifies whether or not to track clients. + - `closeTimeout` {Number} Duration in milliseconds to wait for a graceful + close after [`websocket.close()`][] is called. If the limit is reached, the + connection is forcibly terminated. Defaults to 30000. - `handleProtocols` {Function} A function which can be used to handle the WebSocket subprotocols. See description below. - `host` {String} The hostname where to bind the server. @@ -304,6 +307,9 @@ This class represents a WebSocket. It extends the `EventEmitter`. the WHATWG standardbut may negatively impact performance. - `autoPong` {Boolean} Specifies whether or not to automatically send a pong in response to a ping. Defaults to `true`. + - `closeTimeout` {Number} Duration in milliseconds to wait for a graceful + close after [`websocket.close()`][] is called. If the limit is reached, the + connection is forcibly terminated. Defaults to 30000. - `finishRequest` {Function} A function which can be used to customize the headers of each HTTP request before it is sent. See description below. - `followRedirects` {Boolean} Whether or not to follow redirects. Defaults to @@ -709,4 +715,5 @@ as configured by the `maxPayload` option. [`request.removeheader()`]: https://nodejs.org/api/http.html#requestremoveheadername [`socket.destroy()`]: https://nodejs.org/api/net.html#net_socket_destroy_error +[`websocket.close()`]: #websocketclosecode-reason [zlib-options]: https://nodejs.org/api/zlib.html#zlib_class_options diff --git a/lib/constants.js b/lib/constants.js index 74214d466..69b2fe3c4 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -7,6 +7,7 @@ if (hasBlob) BINARY_TYPES.push('blob'); module.exports = { BINARY_TYPES, + CLOSE_TIMEOUT: 30000, EMPTY_BUFFER: Buffer.alloc(0), GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', hasBlob, diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 33e09858c..75e04c1d6 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -11,7 +11,7 @@ const extension = require('./extension'); const PerMessageDeflate = require('./permessage-deflate'); const subprotocol = require('./subprotocol'); const WebSocket = require('./websocket'); -const { GUID, kWebSocket } = require('./constants'); +const { CLOSE_TIMEOUT, GUID, kWebSocket } = require('./constants'); const keyRegex = /^[+/0-9A-Za-z]{22}==$/; @@ -38,6 +38,9 @@ class WebSocketServer extends EventEmitter { * pending connections * @param {Boolean} [options.clientTracking=true] Specifies whether or not to * track clients + * @param {Number} [options.closeTimeout=30000] Duration in milliseconds to + * wait for the closing handshake to finish after `websocket.close()` is + * called * @param {Function} [options.handleProtocols] A hook to handle protocols * @param {String} [options.host] The hostname where to bind the server * @param {Number} [options.maxPayload=104857600] The maximum allowed message @@ -67,6 +70,7 @@ class WebSocketServer extends EventEmitter { perMessageDeflate: false, handleProtocols: null, clientTracking: true, + closeTimeout: CLOSE_TIMEOUT, verifyClient: null, noServer: false, backlog: null, // use default (511 as implemented in net.js) diff --git a/lib/websocket.js b/lib/websocket.js index ad8764a02..28229e890 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -18,6 +18,7 @@ const { isBlob } = require('./validation'); const { BINARY_TYPES, + CLOSE_TIMEOUT, EMPTY_BUFFER, GUID, kForOnEventAttribute, @@ -32,7 +33,6 @@ const { const { format, parse } = require('./extension'); const { toBuffer } = require('./buffer-util'); -const closeTimeout = 30 * 1000; const kAborted = Symbol('kAborted'); const protocolVersions = [8, 13]; const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; @@ -88,6 +88,7 @@ class WebSocket extends EventEmitter { initAsClient(this, address, protocols, options); } else { this._autoPong = options.autoPong; + this._closeTimeout = options.closeTimeout; this._isServer = true; } } @@ -629,6 +630,8 @@ module.exports = WebSocket; * times in the same tick * @param {Boolean} [options.autoPong=true] Specifies whether or not to * automatically send a pong in response to a ping + * @param {Number} [options.closeTimeout=30000] Duration in milliseconds to wait + * for the closing handshake to finish after `websocket.close()` is called * @param {Function} [options.finishRequest] A function which can be used to * customize the headers of each http request before it is sent * @param {Boolean} [options.followRedirects=false] Whether or not to follow @@ -655,6 +658,7 @@ function initAsClient(websocket, address, protocols, options) { const opts = { allowSynchronousEvents: true, autoPong: true, + closeTimeout: CLOSE_TIMEOUT, protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, @@ -673,6 +677,7 @@ function initAsClient(websocket, address, protocols, options) { }; websocket._autoPong = opts.autoPong; + websocket._closeTimeout = opts.closeTimeout; if (!protocolVersions.includes(opts.protocolVersion)) { throw new RangeError( @@ -1290,7 +1295,7 @@ function senderOnError(err) { function setCloseTimer(websocket) { websocket._closeTimer = setTimeout( websocket._socket.destroy.bind(websocket._socket), - closeTimeout + websocket._closeTimeout ); } diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index db8d62da5..4d5201735 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -140,6 +140,22 @@ describe('WebSocketServer', () => { }); }); }); + + it('honors the `closeTimeout` option', (done) => { + const closeTimeout = 1000; + const wss = new WebSocket.Server({ closeTimeout, port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + ws.on('close', () => { + wss.close(done); + }); + + ws.close(); + assert.strictEqual(ws._closeTimer._idleTimeout, closeTimeout); + }); + }); }); it('emits an error if http server bind fails', (done) => { diff --git a/test/websocket.test.js b/test/websocket.test.js index 32b9d1b5c..012f7c0a6 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -232,6 +232,24 @@ describe('WebSocket', () => { ws.ping(); }); }); + + it('honors the `closeTimeout` option', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const closeTimeout = 1000; + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + closeTimeout + }); + + ws.on('open', () => { + ws.close(); + assert.strictEqual(ws._closeTimer._idleTimeout, closeTimeout); + }); + + ws.on('close', () => { + wss.close(done); + }); + }); + }); }); });