Skip to content

Commit 76087fb

Browse files
committed
[fix] Do not rely on undocumented behavior
Use the chunk returned by `socket.read()` to handle the buffered data instead of relying on a `'data'` event emitted after the `'close'` event. Refs: nodejs/node#39639
1 parent 4c1849a commit 76087fb

File tree

2 files changed

+92
-6
lines changed

2 files changed

+92
-6
lines changed

lib/websocket.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,24 +1002,33 @@ function socketOnClose() {
10021002
const websocket = this[kWebSocket];
10031003

10041004
this.removeListener('close', socketOnClose);
1005+
this.removeListener('data', socketOnData);
10051006
this.removeListener('end', socketOnEnd);
10061007

10071008
websocket._readyState = WebSocket.CLOSING;
10081009

1010+
let chunk;
1011+
10091012
//
10101013
// The close frame might not have been received or the `'end'` event emitted,
10111014
// for example, if the socket was destroyed due to an error. Ensure that the
10121015
// `receiver` stream is closed after writing any remaining buffered data to
10131016
// it. If the readable side of the socket is in flowing mode then there is no
10141017
// buffered data as everything has been already written and `readable.read()`
10151018
// will return `null`. If instead, the socket is paused, any possible buffered
1016-
// data will be read as a single chunk and emitted synchronously in a single
1017-
// `'data'` event.
1019+
// data will be read as a single chunk.
10181020
//
1019-
websocket._socket.read();
1021+
if (
1022+
!this._readableState.endEmitted &&
1023+
!websocket._closeFrameReceived &&
1024+
!websocket._receiver._writableState.errorEmitted &&
1025+
(chunk = websocket._socket.read()) !== null
1026+
) {
1027+
websocket._receiver.write(chunk);
1028+
}
1029+
10201030
websocket._receiver.end();
10211031

1022-
this.removeListener('data', socketOnData);
10231032
this[kWebSocket] = undefined;
10241033

10251034
clearTimeout(websocket._closeTimer);

test/websocket.test.js

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const tls = require('tls');
1010
const fs = require('fs');
1111
const { URL } = require('url');
1212

13+
const Sender = require('../lib/sender');
1314
const WebSocket = require('..');
1415
const { GUID, NOOP } = require('../lib/constants');
1516

@@ -2735,15 +2736,21 @@ describe('WebSocket', () => {
27352736
});
27362737
});
27372738

2738-
it('consumes all received data when connection is closed abnormally', (done) => {
2739+
it('consumes all received data when connection is closed (1/2)', (done) => {
27392740
const wss = new WebSocket.Server(
27402741
{
27412742
perMessageDeflate: { threshold: 0 },
27422743
port: 0
27432744
},
27442745
() => {
2745-
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
27462746
const messages = [];
2747+
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
2748+
2749+
ws.on('open', () => {
2750+
ws._socket.on('close', () => {
2751+
assert.strictEqual(ws._receiver._state, 5);
2752+
});
2753+
});
27472754

27482755
ws.on('message', (message) => messages.push(message));
27492756
ws.on('close', (code) => {
@@ -2762,6 +2769,76 @@ describe('WebSocket', () => {
27622769
});
27632770
});
27642771

2772+
it('consumes all received data when connection is closed (2/2)', (done) => {
2773+
const payload1 = Buffer.alloc(15 * 1024);
2774+
const payload2 = Buffer.alloc(1);
2775+
2776+
const opts = {
2777+
fin: true,
2778+
opcode: 0x02,
2779+
mask: false,
2780+
readOnly: false
2781+
};
2782+
2783+
const list = [
2784+
...Sender.frame(payload1, { rsv1: false, ...opts }),
2785+
...Sender.frame(payload2, { rsv1: true, ...opts })
2786+
];
2787+
2788+
for (let i = 0; i < 399; i++) {
2789+
list.push(list[list.length - 2], list[list.length - 1]);
2790+
}
2791+
2792+
const data = Buffer.concat(list);
2793+
2794+
const wss = new WebSocket.Server(
2795+
{
2796+
perMessageDeflate: true,
2797+
port: 0
2798+
},
2799+
() => {
2800+
const messageLengths = [];
2801+
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
2802+
2803+
ws.on('open', () => {
2804+
ws._socket.prependListener('close', () => {
2805+
assert.strictEqual(ws._receiver._state, 5);
2806+
assert.strictEqual(ws._socket._readableState.length, 3);
2807+
});
2808+
2809+
const push = ws._socket.push;
2810+
2811+
ws._socket.push = (data) => {
2812+
ws._socket.push = push;
2813+
ws._socket.push(data);
2814+
ws.terminate();
2815+
};
2816+
2817+
// This hack is used because there is no guarantee that more than
2818+
// 16 KiB will be sent as a single TCP packet.
2819+
push.call(ws._socket, data);
2820+
2821+
wss.clients
2822+
.values()
2823+
.next()
2824+
.value.send(payload2, { compress: false });
2825+
});
2826+
2827+
ws.on('message', (message) => {
2828+
messageLengths.push(message.length);
2829+
});
2830+
2831+
ws.on('close', (code) => {
2832+
assert.strictEqual(code, 1006);
2833+
assert.strictEqual(messageLengths.length, 402);
2834+
assert.strictEqual(messageLengths[0], 15360);
2835+
assert.strictEqual(messageLengths[messageLengths.length - 1], 1);
2836+
wss.close(done);
2837+
});
2838+
}
2839+
);
2840+
});
2841+
27652842
it('handles a close frame received while compressing data', (done) => {
27662843
const wss = new WebSocket.Server(
27672844
{

0 commit comments

Comments
 (0)