Skip to content

Commit 85ca3d2

Browse files
committed
[major] Make WebSocketServer#close() close connections cleanly
Instead of abruptly closing all WebSocket connections, try to close them cleanly using the 1001 status code. If the HTTP/S server was created internally, then close it and emit the `'close'` event when it closes. Otherwise, if client tracking is enabled, then emit the `'close'` event when the number of connections goes down to zero. Otherwise, emit it in the next tick. Refs: #1902
1 parent 5c725ae commit 85ca3d2

File tree

5 files changed

+92
-72
lines changed

5 files changed

+92
-72
lines changed

doc/ws.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ added when the `clientTracking` is truthy.
209209

210210
Close the HTTP server if created internally, terminate all clients and call
211211
callback when done. If an external HTTP server is used via the `server` or
212-
`noServer` constructor options, it must be closed manually.
212+
`noServer` constructor options, it must be closed manually. The callback is
213+
called with an `Error` if the server is already closed.
213214

214215
### server.handleUpgrade(request, socket, head, callback)
215216

lib/websocket-server.js

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,11 @@ class WebSocketServer extends EventEmitter {
111111
}
112112

113113
if (options.perMessageDeflate === true) options.perMessageDeflate = {};
114-
if (options.clientTracking) this.clients = new Set();
114+
if (options.clientTracking) {
115+
this.clients = new Set();
116+
this._shouldEmitClose = false;
117+
}
118+
115119
this.options = options;
116120
this._state = RUNNING;
117121
}
@@ -151,29 +155,41 @@ class WebSocketServer extends EventEmitter {
151155
if (this._state === CLOSING) return;
152156
this._state = CLOSING;
153157

154-
//
155-
// Terminate all associated clients.
156-
//
157-
if (this.clients) {
158-
for (const client of this.clients) client.terminate();
159-
}
158+
if (this.options.noServer || this.options.server) {
159+
if (this._server) {
160+
this._removeListeners();
161+
this._removeListeners = this._server = null;
162+
}
163+
164+
if (this.clients) {
165+
if (!this.clients.size) {
166+
process.nextTick(emitClose, this);
167+
} else {
168+
for (const client of this.clients) client.close(1001);
160169

161-
const server = this._server;
170+
this._shouldEmitClose = true;
171+
}
172+
} else {
173+
process.nextTick(emitClose, this);
174+
}
175+
} else {
176+
const server = this._server;
162177

163-
if (server) {
164178
this._removeListeners();
165179
this._removeListeners = this._server = null;
166180

181+
if (this.clients) {
182+
for (const client of this.clients) client.close(1001);
183+
}
184+
167185
//
168-
// Close the http server if it was internally created.
186+
// The HTTP/S server was created internally. Close it, and rely on its
187+
// `'close'` event.
169188
//
170-
if (this.options.port != null) {
171-
server.close(emitClose.bind(undefined, this));
172-
return;
173-
}
189+
server.close(() => {
190+
emitClose(this);
191+
});
174192
}
175-
176-
process.nextTick(emitClose, this);
177193
}
178194

179195
/**
@@ -373,7 +389,13 @@ class WebSocketServer extends EventEmitter {
373389

374390
if (this.clients) {
375391
this.clients.add(ws);
376-
ws.on('close', () => this.clients.delete(ws));
392+
ws.on('close', () => {
393+
this.clients.delete(ws);
394+
395+
if (this._shouldEmitClose && !this.clients.size) {
396+
process.nextTick(emitClose, this);
397+
}
398+
});
377399
}
378400

379401
cb(ws, req);

test/sender.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,31 @@ describe('Sender', () => {
266266
});
267267

268268
describe('#close', () => {
269+
it('throws an error if the first argument is invalid', () => {
270+
const mockSocket = new MockSocket();
271+
const sender = new Sender(mockSocket);
272+
273+
assert.throws(
274+
() => sender.close('error'),
275+
/^TypeError: First argument must be a valid error code number$/
276+
);
277+
278+
assert.throws(
279+
() => sender.close(1004),
280+
/^TypeError: First argument must be a valid error code number$/
281+
);
282+
});
283+
284+
it('throws an error if the message is greater than 123 bytes', () => {
285+
const mockSocket = new MockSocket();
286+
const sender = new Sender(mockSocket);
287+
288+
assert.throws(
289+
() => sender.close(1000, 'a'.repeat(124)),
290+
/^RangeError: The message must not be greater than 123 bytes$/
291+
);
292+
});
293+
269294
it('should consume all data before closing', (done) => {
270295
const perMessageDeflate = new PerMessageDeflate({ threshold: 0 });
271296

test/websocket-server.test.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const os = require('os');
1313

1414
const Sender = require('../lib/sender');
1515
const WebSocket = require('..');
16-
const { NOOP } = require('../lib/constants');
16+
const { EMPTY_BUFFER, NOOP } = require('../lib/constants');
1717

1818
describe('WebSocketServer', () => {
1919
describe('#ctor', () => {
@@ -223,13 +223,19 @@ describe('WebSocketServer', () => {
223223
let closes = 0;
224224
const wss = new WebSocket.Server({ port: 0 }, () => {
225225
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
226-
ws.on('close', () => {
226+
ws.on('close', (code, reason) => {
227+
assert.strictEqual(code, 1001);
228+
assert.deepStrictEqual(reason, EMPTY_BUFFER);
229+
227230
if (++closes === 2) done();
228231
});
229232
});
230233

231234
wss.on('connection', (ws) => {
232-
ws.on('close', () => {
235+
ws.on('close', (code, reason) => {
236+
assert.strictEqual(code, 1001);
237+
assert.deepStrictEqual(reason, EMPTY_BUFFER);
238+
233239
if (++closes === 2) done();
234240
});
235241
wss.close();
@@ -309,6 +315,16 @@ describe('WebSocketServer', () => {
309315
});
310316
});
311317

318+
it("emits the 'close' event if client tracking is disabled", (done) => {
319+
const wss = new WebSocket.Server({
320+
noServer: true,
321+
clientTracking: false
322+
});
323+
324+
wss.on('close', done);
325+
wss.close();
326+
});
327+
312328
it("emits the 'close' event if the server is already closed", (done) => {
313329
const wss = new WebSocket.Server({ port: 0 }, () => {
314330
wss.close(() => {
@@ -683,6 +699,7 @@ describe('WebSocketServer', () => {
683699

684700
socket.once('data', (chunk) => {
685701
assert.strictEqual(chunk[0], 0x88);
702+
socket.destroy();
686703
wss.close(done);
687704
});
688705
});

test/websocket.test.js

Lines changed: 6 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1743,51 +1743,6 @@ describe('WebSocket', () => {
17431743
});
17441744
});
17451745

1746-
it('throws an error if the first argument is invalid (1/2)', (done) => {
1747-
const wss = new WebSocket.Server({ port: 0 }, () => {
1748-
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
1749-
1750-
ws.on('open', () => {
1751-
assert.throws(
1752-
() => ws.close('error'),
1753-
/^TypeError: First argument must be a valid error code number$/
1754-
);
1755-
1756-
wss.close(done);
1757-
});
1758-
});
1759-
});
1760-
1761-
it('throws an error if the first argument is invalid (2/2)', (done) => {
1762-
const wss = new WebSocket.Server({ port: 0 }, () => {
1763-
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
1764-
1765-
ws.on('open', () => {
1766-
assert.throws(
1767-
() => ws.close(1004),
1768-
/^TypeError: First argument must be a valid error code number$/
1769-
);
1770-
1771-
wss.close(done);
1772-
});
1773-
});
1774-
});
1775-
1776-
it('throws an error if the message is greater than 123 bytes', (done) => {
1777-
const wss = new WebSocket.Server({ port: 0 }, () => {
1778-
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
1779-
1780-
ws.on('open', () => {
1781-
assert.throws(
1782-
() => ws.close(1000, 'a'.repeat(124)),
1783-
/^RangeError: The message must not be greater than 123 bytes$/
1784-
);
1785-
1786-
wss.close(done);
1787-
});
1788-
});
1789-
});
1790-
17911746
it('sends the close status code only when necessary', (done) => {
17921747
let sent;
17931748
const wss = new WebSocket.Server({ port: 0 }, () => {
@@ -2934,10 +2889,10 @@ describe('WebSocket', () => {
29342889
});
29352890

29362891
it('calls the callback if the socket is closed prematurely', (done) => {
2892+
const called = [];
29372893
const wss = new WebSocket.Server(
29382894
{ perMessageDeflate: true, port: 0 },
29392895
() => {
2940-
const called = [];
29412896
const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
29422897
perMessageDeflate: { threshold: 0 }
29432898
});
@@ -2966,15 +2921,15 @@ describe('WebSocket', () => {
29662921
);
29672922
});
29682923
});
2969-
2970-
ws.on('close', () => {
2971-
assert.deepStrictEqual(called, [1, 2]);
2972-
wss.close(done);
2973-
});
29742924
}
29752925
);
29762926

29772927
wss.on('connection', (ws) => {
2928+
ws.on('close', () => {
2929+
assert.deepStrictEqual(called, [1, 2]);
2930+
wss.close(done);
2931+
});
2932+
29782933
ws._socket.end();
29792934
});
29802935
});

0 commit comments

Comments
 (0)