From 79d31f84233de43bd889d052a23243dc81d2bea4 Mon Sep 17 00:00:00 2001 From: Tony <> Date: Tue, 16 Dec 2025 00:37:31 -0800 Subject: [PATCH 1/4] Added closeTimeout to options. This lets you override the default closeTimeout. --- lib/websocket.js | 3 ++- test/websocket.test.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/websocket.js b/lib/websocket.js index ad8764a02..8fc205521 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -673,6 +673,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 +1291,7 @@ function senderOnError(err) { function setCloseTimer(websocket) { websocket._closeTimer = setTimeout( websocket._socket.destroy.bind(websocket._socket), - closeTimeout + websocket._closeTimeout || closeTimeout ); } diff --git a/test/websocket.test.js b/test/websocket.test.js index 32b9d1b5c..ca53a3610 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -3454,6 +3454,41 @@ describe('WebSocket', () => { }); }); }); + + it('uses closeTimeout milliseconds for the close timer', (done) => { + const timeoutMs = 5 * 1000; + + const wss = new WebSocket.Server({ port: 0, closeTimeout: timeoutMs}, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, undefined, {closeTimeout: timeoutMs}); + + assert.strictEqual(ws._closeTimeout, timeoutMs); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1000); + assert.deepStrictEqual(reason, Buffer.from('some reason')); + wss.close(done); + }); + + ws.on('open', () => { + let callbackCalled = false; + + assert.strictEqual(ws._closeTimer, null); + + ws.send('foo', () => { + callbackCalled = true; + }); + + ws.close(1000, 'some reason'); + + // + // Check that the close timer is set even if the `Sender.close()` + // callback is not called. + // + assert.strictEqual(callbackCalled, false); + assert.strictEqual(ws._closeTimer._idleTimeout, timeoutMs); + }); + }); + }); }); describe('#terminate', () => { From 851f1fc1d7abf71a2dea3eb369515793111cd7a2 Mon Sep 17 00:00:00 2001 From: Tony <> Date: Wed, 17 Dec 2025 17:32:50 -0800 Subject: [PATCH 2/4] Added documentation for closeTimeout. --- doc/ws.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/ws.md b/doc/ws.md index 04c785feb..7263a41aa 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -80,6 +80,7 @@ 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} Timeout in milliseconds for graceful close. - `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 +305,7 @@ 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} Timeout in milliseconds for graceful close. - `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 From bb60b5bf6ca3656bc0d141529cffe7ec059c2a53 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 18 Dec 2025 10:04:52 +0100 Subject: [PATCH 3/4] Make the option work on the server, add JSDoc, and simplify tests --- doc/ws.md | 6 ++-- lib/constants.js | 1 + lib/websocket-server.js | 5 +++- lib/websocket.js | 8 ++++-- test/websocket-server.test.js | 16 +++++++++++ test/websocket.test.js | 53 ++++++++++++----------------------- 6 files changed, 49 insertions(+), 40 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 7263a41aa..724021f0a 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -80,7 +80,8 @@ 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} Timeout in milliseconds for graceful close. + - `closeTimeout` {Number} Timeout in milliseconds for graceful close. 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. @@ -305,7 +306,8 @@ 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} Timeout in milliseconds for graceful close. + - `closeTimeout` {Number} Timeout in milliseconds for graceful close. 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 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..517a720fe 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,8 @@ class WebSocketServer extends EventEmitter { * pending connections * @param {Boolean} [options.clientTracking=true] Specifies whether or not to * track clients + * @param {Number} [options.closeTimeout=30000] Timeout in milliseconds for + * graceful close * @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 +69,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 8fc205521..75fe3c036 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] Timeout in milliseconds for + * graceful close * @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, @@ -1291,7 +1295,7 @@ function senderOnError(err) { function setCloseTimer(websocket) { websocket._closeTimer = setTimeout( websocket._socket.destroy.bind(websocket._socket), - websocket._closeTimeout || 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 ca53a3610..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); + }); + }); + }); }); }); @@ -3454,41 +3472,6 @@ describe('WebSocket', () => { }); }); }); - - it('uses closeTimeout milliseconds for the close timer', (done) => { - const timeoutMs = 5 * 1000; - - const wss = new WebSocket.Server({ port: 0, closeTimeout: timeoutMs}, () => { - const ws = new WebSocket(`ws://localhost:${wss.address().port}`, undefined, {closeTimeout: timeoutMs}); - - assert.strictEqual(ws._closeTimeout, timeoutMs); - - ws.on('close', (code, reason) => { - assert.strictEqual(code, 1000); - assert.deepStrictEqual(reason, Buffer.from('some reason')); - wss.close(done); - }); - - ws.on('open', () => { - let callbackCalled = false; - - assert.strictEqual(ws._closeTimer, null); - - ws.send('foo', () => { - callbackCalled = true; - }); - - ws.close(1000, 'some reason'); - - // - // Check that the close timer is set even if the `Sender.close()` - // callback is not called. - // - assert.strictEqual(callbackCalled, false); - assert.strictEqual(ws._closeTimer._idleTimeout, timeoutMs); - }); - }); - }); }); describe('#terminate', () => { From dc6dcb6d2778ca2be8c934777a6769f9f868f2fc Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 2 Jan 2026 18:50:51 +0100 Subject: [PATCH 4/4] fixup! Make the option work on the server, add JSDoc, and simplify tests --- doc/ws.md | 11 +++++++---- lib/websocket-server.js | 5 +++-- lib/websocket.js | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index 724021f0a..7d22a0480 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -80,8 +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} Timeout in milliseconds for graceful close. Defaults - to 30000. + - `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. @@ -306,8 +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} Timeout in milliseconds for graceful close. Defaults - to 30000. + - `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 @@ -713,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/websocket-server.js b/lib/websocket-server.js index 517a720fe..75e04c1d6 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -38,8 +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] Timeout in milliseconds for - * graceful close + * @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 diff --git a/lib/websocket.js b/lib/websocket.js index 75fe3c036..28229e890 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -630,8 +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] Timeout in milliseconds for - * graceful close + * @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