Skip to content

Commit 6f9ca82

Browse files
[feat] Introduce the closeTimeout option (#2308)
Make graceful close timeout configurable.
1 parent 1998485 commit 6f9ca82

File tree

6 files changed

+54
-3
lines changed

6 files changed

+54
-3
lines changed

doc/ws.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ This class represents a WebSocket server. It extends the `EventEmitter`.
8080
in response to a ping. Defaults to `true`.
8181
- `backlog` {Number} The maximum length of the queue of pending connections.
8282
- `clientTracking` {Boolean} Specifies whether or not to track clients.
83+
- `closeTimeout` {Number} Duration in milliseconds to wait for a graceful
84+
close after [`websocket.close()`][] is called. If the limit is reached, the
85+
connection is forcibly terminated. Defaults to 30000.
8386
- `handleProtocols` {Function} A function which can be used to handle the
8487
WebSocket subprotocols. See description below.
8588
- `host` {String} The hostname where to bind the server.
@@ -304,6 +307,9 @@ This class represents a WebSocket. It extends the `EventEmitter`.
304307
the WHATWG standardbut may negatively impact performance.
305308
- `autoPong` {Boolean} Specifies whether or not to automatically send a pong
306309
in response to a ping. Defaults to `true`.
310+
- `closeTimeout` {Number} Duration in milliseconds to wait for a graceful
311+
close after [`websocket.close()`][] is called. If the limit is reached, the
312+
connection is forcibly terminated. Defaults to 30000.
307313
- `finishRequest` {Function} A function which can be used to customize the
308314
headers of each HTTP request before it is sent. See description below.
309315
- `followRedirects` {Boolean} Whether or not to follow redirects. Defaults to
@@ -709,4 +715,5 @@ as configured by the `maxPayload` option.
709715
[`request.removeheader()`]:
710716
https://nodejs.org/api/http.html#requestremoveheadername
711717
[`socket.destroy()`]: https://nodejs.org/api/net.html#net_socket_destroy_error
718+
[`websocket.close()`]: #websocketclosecode-reason
712719
[zlib-options]: https://nodejs.org/api/zlib.html#zlib_class_options

lib/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ if (hasBlob) BINARY_TYPES.push('blob');
77

88
module.exports = {
99
BINARY_TYPES,
10+
CLOSE_TIMEOUT: 30000,
1011
EMPTY_BUFFER: Buffer.alloc(0),
1112
GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
1213
hasBlob,

lib/websocket-server.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const extension = require('./extension');
1111
const PerMessageDeflate = require('./permessage-deflate');
1212
const subprotocol = require('./subprotocol');
1313
const WebSocket = require('./websocket');
14-
const { GUID, kWebSocket } = require('./constants');
14+
const { CLOSE_TIMEOUT, GUID, kWebSocket } = require('./constants');
1515

1616
const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
1717

@@ -38,6 +38,9 @@ class WebSocketServer extends EventEmitter {
3838
* pending connections
3939
* @param {Boolean} [options.clientTracking=true] Specifies whether or not to
4040
* track clients
41+
* @param {Number} [options.closeTimeout=30000] Duration in milliseconds to
42+
* wait for the closing handshake to finish after `websocket.close()` is
43+
* called
4144
* @param {Function} [options.handleProtocols] A hook to handle protocols
4245
* @param {String} [options.host] The hostname where to bind the server
4346
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
@@ -67,6 +70,7 @@ class WebSocketServer extends EventEmitter {
6770
perMessageDeflate: false,
6871
handleProtocols: null,
6972
clientTracking: true,
73+
closeTimeout: CLOSE_TIMEOUT,
7074
verifyClient: null,
7175
noServer: false,
7276
backlog: null, // use default (511 as implemented in net.js)

lib/websocket.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const { isBlob } = require('./validation');
1818

1919
const {
2020
BINARY_TYPES,
21+
CLOSE_TIMEOUT,
2122
EMPTY_BUFFER,
2223
GUID,
2324
kForOnEventAttribute,
@@ -32,7 +33,6 @@ const {
3233
const { format, parse } = require('./extension');
3334
const { toBuffer } = require('./buffer-util');
3435

35-
const closeTimeout = 30 * 1000;
3636
const kAborted = Symbol('kAborted');
3737
const protocolVersions = [8, 13];
3838
const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
@@ -88,6 +88,7 @@ class WebSocket extends EventEmitter {
8888
initAsClient(this, address, protocols, options);
8989
} else {
9090
this._autoPong = options.autoPong;
91+
this._closeTimeout = options.closeTimeout;
9192
this._isServer = true;
9293
}
9394
}
@@ -629,6 +630,8 @@ module.exports = WebSocket;
629630
* times in the same tick
630631
* @param {Boolean} [options.autoPong=true] Specifies whether or not to
631632
* automatically send a pong in response to a ping
633+
* @param {Number} [options.closeTimeout=30000] Duration in milliseconds to wait
634+
* for the closing handshake to finish after `websocket.close()` is called
632635
* @param {Function} [options.finishRequest] A function which can be used to
633636
* customize the headers of each http request before it is sent
634637
* @param {Boolean} [options.followRedirects=false] Whether or not to follow
@@ -655,6 +658,7 @@ function initAsClient(websocket, address, protocols, options) {
655658
const opts = {
656659
allowSynchronousEvents: true,
657660
autoPong: true,
661+
closeTimeout: CLOSE_TIMEOUT,
658662
protocolVersion: protocolVersions[1],
659663
maxPayload: 100 * 1024 * 1024,
660664
skipUTF8Validation: false,
@@ -673,6 +677,7 @@ function initAsClient(websocket, address, protocols, options) {
673677
};
674678

675679
websocket._autoPong = opts.autoPong;
680+
websocket._closeTimeout = opts.closeTimeout;
676681

677682
if (!protocolVersions.includes(opts.protocolVersion)) {
678683
throw new RangeError(
@@ -1290,7 +1295,7 @@ function senderOnError(err) {
12901295
function setCloseTimer(websocket) {
12911296
websocket._closeTimer = setTimeout(
12921297
websocket._socket.destroy.bind(websocket._socket),
1293-
closeTimeout
1298+
websocket._closeTimeout
12941299
);
12951300
}
12961301

test/websocket-server.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,22 @@ describe('WebSocketServer', () => {
140140
});
141141
});
142142
});
143+
144+
it('honors the `closeTimeout` option', (done) => {
145+
const closeTimeout = 1000;
146+
const wss = new WebSocket.Server({ closeTimeout, port: 0 }, () => {
147+
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
148+
});
149+
150+
wss.on('connection', (ws) => {
151+
ws.on('close', () => {
152+
wss.close(done);
153+
});
154+
155+
ws.close();
156+
assert.strictEqual(ws._closeTimer._idleTimeout, closeTimeout);
157+
});
158+
});
143159
});
144160

145161
it('emits an error if http server bind fails', (done) => {

test/websocket.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,24 @@ describe('WebSocket', () => {
232232
ws.ping();
233233
});
234234
});
235+
236+
it('honors the `closeTimeout` option', (done) => {
237+
const wss = new WebSocket.Server({ port: 0 }, () => {
238+
const closeTimeout = 1000;
239+
const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
240+
closeTimeout
241+
});
242+
243+
ws.on('open', () => {
244+
ws.close();
245+
assert.strictEqual(ws._closeTimer._idleTimeout, closeTimeout);
246+
});
247+
248+
ws.on('close', () => {
249+
wss.close(done);
250+
});
251+
});
252+
});
235253
});
236254
});
237255

0 commit comments

Comments
 (0)