Skip to content

Commit fb58179

Browse files
committed
[minor] Simplify the closing handshake
- When the socket emits the `'end'` event, do not call `socket.end()`. End only the `receiver` stream. - Do not wait for a close frame to be received and sent before calling `socket.end()`. Call it right after the close frame is sent. - When the `receiver` stream emits `'finish'`, send a close frame if no close frame is received. The assumption is that the socket is allowed to be half-open. On the server side this is always true (unless the user explicitly sets the `allowHalfOpen` property of the socket to `false`). On the client side the user might use an agent so we set `socket.allowHalfOpen` to `true` when the `http.ClientRequest` object emits the `'upgrade'` event. Refs: #1899
1 parent 1d3f4cb commit fb58179

File tree

3 files changed

+62
-38
lines changed

3 files changed

+62
-38
lines changed

lib/websocket.js

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ class WebSocket extends EventEmitter {
123123
* @type {Number}
124124
*/
125125
get readyState() {
126-
return this._readyState;
126+
return this._readyState === -2 ? WebSocket.CLOSING : this._readyState;
127127
}
128128

129129
/**
@@ -159,6 +159,7 @@ class WebSocket extends EventEmitter {
159159
receiver.on('conclude', receiverOnConclude);
160160
receiver.on('drain', receiverOnDrain);
161161
receiver.on('error', receiverOnError);
162+
receiver.on('finish', receiverOnFinish);
162163
receiver.on('message', receiverOnMessage);
163164
receiver.on('ping', receiverOnPing);
164165
receiver.on('pong', receiverOnPong);
@@ -201,31 +202,25 @@ class WebSocket extends EventEmitter {
201202
/**
202203
* Start a closing handshake.
203204
*
204-
* +----------+ +-----------+ +----------+
205-
* - - -|ws.close()|-->|close frame|-->|ws.close()|- - -
206-
* | +----------+ +-----------+ +----------+ |
207-
* +----------+ +-----------+ |
208-
* CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING
209-
* +----------+ +-----------+ |
210-
* | | | +---+ |
211-
* +------------------------+-->|fin| - - - -
212-
* | +---+ | +---+
213-
* - - - - -|fin|<---------------------+
214-
* +---+
215-
*
216205
* @param {Number} [code] Status code explaining why the connection is closing
217206
* @param {String} [data] A string explaining why the connection is closing
218207
* @public
219208
*/
220209
close(code, data) {
221-
if (this.readyState === WebSocket.CLOSED) return;
222210
if (this.readyState === WebSocket.CONNECTING) {
223211
const msg = 'WebSocket was closed before the connection was established';
224212
return abortHandshake(this, this._req, msg);
225213
}
226214

227-
if (this.readyState === WebSocket.CLOSING) {
228-
if (this._closeFrameSent && this._closeFrameReceived) this._socket.end();
215+
//
216+
// `this._readyState` below is not a typo. The
217+
// `WebSocket.prototype.readyState` getter returns `WebSocket.CLOSING` even
218+
// if `this._readyState` is `-2`.
219+
//
220+
if (
221+
this._readyState === WebSocket.CLOSING ||
222+
this.readyState === WebSocket.CLOSED
223+
) {
229224
return;
230225
}
231226

@@ -238,7 +233,7 @@ class WebSocket extends EventEmitter {
238233
if (err) return;
239234

240235
this._closeFrameSent = true;
241-
if (this._closeFrameReceived) this._socket.end();
236+
this._socket.end();
242237
});
243238

244239
//
@@ -610,6 +605,15 @@ function initAsClient(websocket, address, protocols, options) {
610605
});
611606

612607
req.on('upgrade', (res, socket, head) => {
608+
//
609+
// `tls.connect()` does not support the `allowHalfOpen` option in Node.js <
610+
// 12.9.0. Also, the socket creation is not always under our control as the
611+
// user might use the `http{,s}.request()` `agent` option and by default
612+
// `net.connect()` and `tls.connect()` create a socket not allowed to be
613+
// half-open.
614+
//
615+
socket.allowHalfOpen = true;
616+
613617
websocket.emit('upgrade', res);
614618

615619
//
@@ -679,6 +683,7 @@ function initAsClient(websocket, address, protocols, options) {
679683
* @private
680684
*/
681685
function netConnect(options) {
686+
options.allowHalfOpen = true;
682687
options.path = options.socketPath;
683688
return net.connect(options);
684689
}
@@ -691,6 +696,7 @@ function netConnect(options) {
691696
* @private
692697
*/
693698
function tlsConnect(options) {
699+
options.allowHalfOpen = true;
694700
options.path = undefined;
695701

696702
if (!options.servername && options.servername !== '') {
@@ -820,6 +826,17 @@ function receiverOnError(err) {
820826
* @private
821827
*/
822828
function receiverOnFinish() {
829+
const websocket = this[kWebSocket];
830+
831+
if (!websocket._closeFrameReceived) websocket.close();
832+
}
833+
834+
/**
835+
* A listener for the `Receiver` `'error'` or `'finish'` event.
836+
*
837+
* @private
838+
*/
839+
function receiverOnErrorOrFinish() {
823840
this[kWebSocket].emitClose();
824841
}
825842

@@ -893,8 +910,8 @@ function socketOnClose() {
893910
) {
894911
websocket.emitClose();
895912
} else {
896-
websocket._receiver.on('error', receiverOnFinish);
897-
websocket._receiver.on('finish', receiverOnFinish);
913+
websocket._receiver.on('error', receiverOnErrorOrFinish);
914+
websocket._receiver.on('finish', receiverOnErrorOrFinish);
898915
}
899916
}
900917

@@ -918,9 +935,13 @@ function socketOnData(chunk) {
918935
function socketOnEnd() {
919936
const websocket = this[kWebSocket];
920937

921-
websocket._readyState = WebSocket.CLOSING;
938+
//
939+
// This is a special state. It indicates that the connection is going through
940+
// the closing handshake but unlike `WebSocket.CLOSING` it does not prevent
941+
// `WebSocket.prototype.close()` from sending a close frame.
942+
//
943+
websocket._readyState = -2;
922944
websocket._receiver.end();
923-
this.end();
924945
}
925946

926947
/**

test/create-websocket-stream.test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ describe('createWebSocketStream', () => {
203203
});
204204

205205
it('reemits errors', (done) => {
206-
let duplexCloseEventEmitted = false;
206+
let closeEventCount = 0;
207207
const wss = new WebSocket.Server({ port: 0 }, () => {
208208
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
209209
const duplex = createWebSocketStream(ws);
@@ -217,18 +217,18 @@ describe('createWebSocketStream', () => {
217217
);
218218

219219
duplex.on('close', () => {
220-
duplexCloseEventEmitted = true;
220+
if (++closeEventCount === 2) wss.close(done);
221221
});
222222
});
223223
});
224224

225225
wss.on('connection', (ws) => {
226226
ws._socket.write(Buffer.from([0x85, 0x00]));
227227
ws.on('close', (code, reason) => {
228-
assert.ok(duplexCloseEventEmitted);
229228
assert.strictEqual(code, 1002);
230229
assert.strictEqual(reason, '');
231-
wss.close(done);
230+
231+
if (++closeEventCount === 2) wss.close(done);
232232
});
233233
});
234234
});

test/websocket.test.js

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ describe('WebSocket', () => {
429429

430430
describe('Events', () => {
431431
it("emits an 'error' event if an error occurs", (done) => {
432-
let clientCloseEventEmitted = false;
432+
let closeEventCount = 0;
433433
const wss = new WebSocket.Server({ port: 0 }, () => {
434434
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
435435

@@ -442,19 +442,20 @@ describe('WebSocket', () => {
442442
);
443443

444444
ws.on('close', (code, reason) => {
445-
clientCloseEventEmitted = true;
446445
assert.strictEqual(code, 1006);
447446
assert.strictEqual(reason, '');
447+
448+
if (++closeEventCount === 2) wss.close(done);
448449
});
449450
});
450451
});
451452

452453
wss.on('connection', (ws) => {
453454
ws.on('close', (code, reason) => {
454-
assert.ok(clientCloseEventEmitted);
455455
assert.strictEqual(code, 1002);
456456
assert.strictEqual(reason, '');
457-
wss.close(done);
457+
458+
if (++closeEventCount === 2) wss.close(done);
458459
});
459460

460461
ws._socket.write(Buffer.from([0x85, 0x00]));
@@ -547,6 +548,7 @@ describe('WebSocket', () => {
547548
.update(req.headers['sec-websocket-key'] + GUID)
548549
.digest('base64');
549550

551+
socket.resume();
550552
socket.end(
551553
'HTTP/1.1 101 Switching Protocols\r\n' +
552554
'Upgrade: websocket\r\n' +
@@ -1419,16 +1421,16 @@ describe('WebSocket', () => {
14191421
});
14201422

14211423
it('honors the `mask` option', (done) => {
1422-
let serverClientCloseEventEmitted = false;
1424+
let closeEventCount = 0;
14231425
const wss = new WebSocket.Server({ port: 0 }, () => {
14241426
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
14251427

14261428
ws.on('open', () => ws.send('hi', { mask: false }));
14271429
ws.on('close', (code, reason) => {
1428-
assert.ok(serverClientCloseEventEmitted);
14291430
assert.strictEqual(code, 1002);
14301431
assert.strictEqual(reason, '');
1431-
wss.close(done);
1432+
1433+
if (++closeEventCount === 2) wss.close(done);
14321434
});
14331435
});
14341436

@@ -1450,9 +1452,10 @@ describe('WebSocket', () => {
14501452
);
14511453

14521454
ws.on('close', (code, reason) => {
1453-
serverClientCloseEventEmitted = true;
14541455
assert.strictEqual(code, 1006);
14551456
assert.strictEqual(reason, '');
1457+
1458+
if (++closeEventCount === 2) wss.close(done);
14561459
});
14571460
});
14581461
});
@@ -2624,7 +2627,9 @@ describe('WebSocket', () => {
26242627
ws.send('foo');
26252628
ws.send('bar');
26262629
ws.send('baz');
2627-
ws.send('qux', () => ws._socket.end());
2630+
ws.send('qux', () => {
2631+
ws.terminate();
2632+
});
26282633
});
26292634
});
26302635

@@ -2679,6 +2684,8 @@ describe('WebSocket', () => {
26792684
'The socket was closed while data was being compressed'
26802685
);
26812686
});
2687+
2688+
ws.terminate();
26822689
});
26832690

26842691
ws.on('close', () => {
@@ -2687,10 +2694,6 @@ describe('WebSocket', () => {
26872694
});
26882695
}
26892696
);
2690-
2691-
wss.on('connection', (ws) => {
2692-
ws._socket.end();
2693-
});
26942697
});
26952698
});
26962699

0 commit comments

Comments
 (0)