Skip to content

Commit 6a238da

Browse files
committed
[major] Do not close existing connections
When `WebSocketServer.prototype.close()` is called, stop accepting new connections but do not close the existing ones. 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 6a238da

File tree

5 files changed

+284
-141
lines changed

5 files changed

+284
-141
lines changed

doc/ws.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,14 @@ added when the `clientTracking` is truthy.
207207

208208
### server.close([callback])
209209

210-
Close the HTTP server if created internally, terminate all clients and call
211-
callback when done. If an external HTTP server is used via the `server` or
212-
`noServer` constructor options, it must be closed manually.
210+
Prevent the server from accepting new connections and close the HTTP server if
211+
created internally. If an external HTTP server is used via the `server` or
212+
`noServer` constructor options, it must be closed manually. Existing connections
213+
are not closed automatically. The server emits a `'close'` event when all
214+
connections are closed unless an external HTTP server is used and client
215+
tracking is disabled. In this case the `'close'` event is emitted in the next
216+
tick. The optional callback is called when the `'close'` event occurs and
217+
receives an `Error` if the server is already closed.
213218

214219
### server.handleUpgrade(request, socket, head, callback)
215220

lib/websocket-server.js

Lines changed: 36 additions & 19 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
}
@@ -135,9 +139,10 @@ class WebSocketServer extends EventEmitter {
135139
}
136140

137141
/**
138-
* Close the server.
142+
* Stop the server from accepting new connections and emit the `'close'` event
143+
* when all existing connections are closed.
139144
*
140-
* @param {Function} [cb] Callback
145+
* @param {Function} [cb] A one-time listener for the `'close'` event
141146
* @public
142147
*/
143148
close(cb) {
@@ -151,29 +156,35 @@ class WebSocketServer extends EventEmitter {
151156
if (this._state === CLOSING) return;
152157
this._state = CLOSING;
153158

154-
//
155-
// Terminate all associated clients.
156-
//
157-
if (this.clients) {
158-
for (const client of this.clients) client.terminate();
159-
}
159+
if (this.options.noServer || this.options.server) {
160+
if (this._server) {
161+
this._removeListeners();
162+
this._removeListeners = this._server = null;
163+
}
160164

161-
const server = this._server;
165+
if (this.clients) {
166+
if (!this.clients.size) {
167+
process.nextTick(emitClose, this);
168+
} else {
169+
this._shouldEmitClose = true;
170+
}
171+
} else {
172+
process.nextTick(emitClose, this);
173+
}
174+
} else {
175+
const server = this._server;
162176

163-
if (server) {
164177
this._removeListeners();
165178
this._removeListeners = this._server = null;
166179

167180
//
168-
// Close the http server if it was internally created.
181+
// The HTTP/S server was created internally. Close it, and rely on its
182+
// `'close'` event.
169183
//
170-
if (this.options.port != null) {
171-
server.close(emitClose.bind(undefined, this));
172-
return;
173-
}
184+
server.close(() => {
185+
emitClose(this);
186+
});
174187
}
175-
176-
process.nextTick(emitClose, this);
177188
}
178189

179190
/**
@@ -373,7 +384,13 @@ class WebSocketServer extends EventEmitter {
373384

374385
if (this.clients) {
375386
this.clients.add(ws);
376-
ws.on('close', () => this.clients.delete(ws));
387+
ws.on('close', () => {
388+
this.clients.delete(ws);
389+
390+
if (this._shouldEmitClose && !this.clients.size) {
391+
process.nextTick(emitClose, this);
392+
}
393+
});
377394
}
378395

379396
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: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ describe('WebSocketServer', () => {
7575
},
7676
() => {
7777
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
78+
79+
ws.on('open', ws.close);
7880
}
7981
);
8082

@@ -103,6 +105,8 @@ describe('WebSocketServer', () => {
103105
const port = 1337;
104106
const wss = new WebSocket.Server({ port }, () => {
105107
const ws = new WebSocket(`ws://localhost:${port}`);
108+
109+
ws.on('open', ws.close);
106110
});
107111

108112
wss.on('connection', () => wss.close(done));
@@ -120,12 +124,14 @@ describe('WebSocketServer', () => {
120124

121125
server.listen(0, () => {
122126
const wss = new WebSocket.Server({ server });
123-
const ws = new WebSocket(`ws://localhost:${server.address().port}`);
124127

125128
wss.on('connection', () => {
126-
wss.close();
127129
server.close(done);
128130
});
131+
132+
const ws = new WebSocket(`ws://localhost:${server.address().port}`);
133+
134+
ws.on('open', ws.close);
129135
});
130136
});
131137

@@ -169,7 +175,11 @@ describe('WebSocketServer', () => {
169175
assert.strictEqual(req.url, '/foo?bar=bar');
170176
} else {
171177
assert.strictEqual(req.url, '/');
172-
wss.close();
178+
179+
for (const client of wss.clients) {
180+
client.close();
181+
}
182+
173183
server.close(done);
174184
}
175185
});
@@ -209,30 +219,13 @@ describe('WebSocketServer', () => {
209219
});
210220

211221
describe('#close', () => {
212-
it('does not throw when called twice', (done) => {
222+
it('does not throw if called multiple times', (done) => {
213223
const wss = new WebSocket.Server({ port: 0 }, () => {
224+
wss.on('close', done);
225+
214226
wss.close();
215227
wss.close();
216228
wss.close();
217-
218-
done();
219-
});
220-
});
221-
222-
it('closes all clients', (done) => {
223-
let closes = 0;
224-
const wss = new WebSocket.Server({ port: 0 }, () => {
225-
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
226-
ws.on('close', () => {
227-
if (++closes === 2) done();
228-
});
229-
});
230-
231-
wss.on('connection', (ws) => {
232-
ws.on('close', () => {
233-
if (++closes === 2) done();
234-
});
235-
wss.close();
236229
});
237230
});
238231

@@ -254,6 +247,8 @@ describe('WebSocketServer', () => {
254247

255248
server.listen(0, () => {
256249
const ws = new WebSocket(`ws://localhost:${server.address().port}`);
250+
251+
ws.on('open', ws.close);
257252
});
258253
});
259254

@@ -309,6 +304,16 @@ describe('WebSocketServer', () => {
309304
});
310305
});
311306

307+
it("emits the 'close' event if client tracking is disabled", (done) => {
308+
const wss = new WebSocket.Server({
309+
noServer: true,
310+
clientTracking: false
311+
});
312+
313+
wss.on('close', done);
314+
wss.close();
315+
});
316+
312317
it("emits the 'close' event if the server is already closed", (done) => {
313318
const wss = new WebSocket.Server({ port: 0 }, () => {
314319
wss.close(() => {
@@ -324,7 +329,10 @@ describe('WebSocketServer', () => {
324329
it('returns a list of connected clients', (done) => {
325330
const wss = new WebSocket.Server({ port: 0 }, () => {
326331
assert.strictEqual(wss.clients.size, 0);
332+
327333
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
334+
335+
ws.on('open', ws.close);
328336
});
329337

330338
wss.on('connection', () => {
@@ -404,17 +412,17 @@ describe('WebSocketServer', () => {
404412
const wss = new WebSocket.Server({ noServer: true });
405413

406414
server.on('upgrade', (req, socket, head) => {
407-
wss.handleUpgrade(req, socket, head, (client) =>
408-
client.send('hello')
409-
);
415+
wss.handleUpgrade(req, socket, head, (ws) => {
416+
ws.send('hello');
417+
ws.close();
418+
});
410419
});
411420

412421
const ws = new WebSocket(`ws://localhost:${server.address().port}`);
413422

414423
ws.on('message', (message, isBinary) => {
415424
assert.deepStrictEqual(message, Buffer.from('hello'));
416425
assert.ok(!isBinary);
417-
wss.close();
418426
server.close(done);
419427
});
420428
});
@@ -683,6 +691,7 @@ describe('WebSocketServer', () => {
683691

684692
socket.once('data', (chunk) => {
685693
assert.strictEqual(chunk[0], 0x88);
694+
socket.destroy();
686695
wss.close(done);
687696
});
688697
});
@@ -742,7 +751,6 @@ describe('WebSocketServer', () => {
742751
});
743752

744753
wss.on('connection', () => {
745-
wss.close();
746754
server.close(done);
747755
});
748756

@@ -751,6 +759,8 @@ describe('WebSocketServer', () => {
751759
headers: { Origin: 'https://example.com', foo: 'bar' },
752760
rejectUnauthorized: false
753761
});
762+
763+
ws.on('open', ws.close);
754764
});
755765
});
756766

@@ -762,6 +772,8 @@ describe('WebSocketServer', () => {
762772
},
763773
() => {
764774
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
775+
776+
ws.on('open', ws.close);
765777
}
766778
);
767779

@@ -959,24 +971,30 @@ describe('WebSocketServer', () => {
959971
wss.close(done);
960972
});
961973
});
974+
975+
wss.on('connection', (ws) => {
976+
ws.close();
977+
});
962978
});
963979
});
964980

965981
it("emits the 'headers' event", (done) => {
966982
const wss = new WebSocket.Server({ port: 0 }, () => {
967983
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
968984

969-
wss.on('headers', (headers, request) => {
970-
assert.deepStrictEqual(headers.slice(0, 3), [
971-
'HTTP/1.1 101 Switching Protocols',
972-
'Upgrade: websocket',
973-
'Connection: Upgrade'
974-
]);
975-
assert.ok(request instanceof http.IncomingMessage);
976-
assert.strictEqual(request.url, '/');
985+
ws.on('open', ws.close);
986+
});
977987

978-
wss.on('connection', () => wss.close(done));
979-
});
988+
wss.on('headers', (headers, request) => {
989+
assert.deepStrictEqual(headers.slice(0, 3), [
990+
'HTTP/1.1 101 Switching Protocols',
991+
'Upgrade: websocket',
992+
'Connection: Upgrade'
993+
]);
994+
assert.ok(request instanceof http.IncomingMessage);
995+
assert.strictEqual(request.url, '/');
996+
997+
wss.on('connection', () => wss.close(done));
980998
});
981999
});
9821000
});
@@ -985,6 +1003,8 @@ describe('WebSocketServer', () => {
9851003
it('is disabled by default', (done) => {
9861004
const wss = new WebSocket.Server({ port: 0 }, () => {
9871005
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
1006+
1007+
ws.on('open', ws.close);
9881008
});
9891009

9901010
wss.on('connection', (ws, req) => {
@@ -1016,6 +1036,10 @@ describe('WebSocketServer', () => {
10161036
});
10171037
}
10181038
);
1039+
1040+
wss.on('connection', (ws) => {
1041+
ws.close();
1042+
});
10191043
});
10201044
});
10211045
});

0 commit comments

Comments
 (0)