Skip to content

Commit ac2dade

Browse files
authored
[fix] Prevent WebSocket#close() from triggering an infinite loop (#969)
This prevents `WebSocket.prototype.close()` from triggering an infinite loop if called from an error listener while connecting.
1 parent bd41a05 commit ac2dade

File tree

2 files changed

+107
-97
lines changed

2 files changed

+107
-97
lines changed

lib/WebSocket.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,12 @@ class WebSocket extends EventEmitter {
263263
close (code, data) {
264264
if (this.readyState === WebSocket.CLOSED) return;
265265
if (this.readyState === WebSocket.CONNECTING) {
266-
this._req.abort();
267-
this.emit('error', new Error('closed before the connection is established'));
268-
return this.finalize(true);
266+
if (this._req && !this._req.aborted) {
267+
this._req.abort();
268+
this.emit('error', new Error('closed before the connection is established'));
269+
this.finalize(true);
270+
}
271+
return;
269272
}
270273

271274
if (this.readyState === WebSocket.CLOSING) {
@@ -377,9 +380,12 @@ class WebSocket extends EventEmitter {
377380
terminate () {
378381
if (this.readyState === WebSocket.CLOSED) return;
379382
if (this.readyState === WebSocket.CONNECTING) {
380-
this._req.abort();
381-
this.emit('error', new Error('closed before the connection is established'));
382-
return this.finalize(true);
383+
if (this._req && !this._req.aborted) {
384+
this._req.abort();
385+
this.emit('error', new Error('closed before the connection is established'));
386+
this.finalize(true);
387+
}
388+
return;
383389
}
384390

385391
if (this._socket) {
@@ -631,6 +637,7 @@ function initAsClient (address, protocols, options) {
631637
this._req.on('error', (error) => {
632638
if (this._req.aborted) return;
633639

640+
this._req = null;
634641
this.emit('error', error);
635642
this.finalize(true);
636643
});
@@ -644,6 +651,8 @@ function initAsClient (address, protocols, options) {
644651
});
645652

646653
this._req.on('upgrade', (res, socket, head) => {
654+
this._req = null;
655+
647656
const digest = crypto.createHash('sha1')
648657
.update(key + GUID, 'binary')
649658
.digest('base64');
@@ -688,7 +697,6 @@ function initAsClient (address, protocols, options) {
688697
this.extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
689698
}
690699

691-
this._req = null;
692700
this.setSocket(socket, head);
693701
});
694702
}

test/WebSocket.test.js

Lines changed: 92 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -305,77 +305,6 @@ describe('WebSocket', function () {
305305
});
306306

307307
describe('connection establishing', function () {
308-
it('can terminate before connection is established (1/2)', function (done) {
309-
const wss = new WebSocketServer({ port: ++port }, () => {
310-
const ws = new WebSocket(`ws://localhost:${port}`);
311-
312-
ws.on('open', () => assert.fail(null, null, 'connect shouldnt be raised here'));
313-
ws.on('error', (err) => {
314-
assert.ok(err instanceof Error);
315-
assert.strictEqual(err.message, 'closed before the connection is established');
316-
ws.on('close', () => wss.close(done));
317-
});
318-
ws.terminate();
319-
});
320-
});
321-
322-
it('can terminate before connection is established (2/2)', function (done) {
323-
const wss = new WebSocketServer({
324-
verifyClient: (info, cb) => setTimeout(cb, 300, true),
325-
port: ++port
326-
}, () => {
327-
const ws = new WebSocket(`ws://localhost:${port}`);
328-
329-
ws.on('open', () => assert.fail(null, null, 'connect shouldnt be raised here'));
330-
ws.on('error', (err) => {
331-
assert.ok(err instanceof Error);
332-
assert.strictEqual(err.message, 'closed before the connection is established');
333-
ws.on('close', () => wss.close(done));
334-
});
335-
setTimeout(() => ws.terminate(), 150);
336-
});
337-
});
338-
339-
it('can close before connection is established (1/2)', function (done) {
340-
const wss = new WebSocketServer({ port: ++port }, () => {
341-
const ws = new WebSocket(`ws://localhost:${port}`);
342-
343-
ws.on('open', () => assert.fail(null, null, 'connect shouldnt be raised here'));
344-
ws.on('error', (err) => {
345-
assert.ok(err instanceof Error);
346-
assert.strictEqual(err.message, 'closed before the connection is established');
347-
ws.on('close', () => wss.close(done));
348-
});
349-
ws.close(1001);
350-
});
351-
});
352-
353-
it('can close before connection is established (2/2)', function (done) {
354-
const wss = new WebSocketServer({
355-
verifyClient: (info, cb) => setTimeout(cb, 300, true),
356-
port: ++port
357-
}, () => {
358-
const ws = new WebSocket(`ws://localhost:${port}`);
359-
360-
ws.on('open', () => assert.fail(null, null, 'connect shouldnt be raised here'));
361-
ws.on('error', (err) => {
362-
assert.ok(err instanceof Error);
363-
assert.strictEqual(err.message, 'closed before the connection is established');
364-
ws.on('close', () => wss.close(done));
365-
});
366-
setTimeout(() => ws.close(1001), 150);
367-
});
368-
});
369-
370-
it('can handle error before request is upgraded', function (done) {
371-
// Here, we don't create a server, to guarantee that the connection will
372-
// fail before the request is upgraded
373-
const ws = new WebSocket(`ws://localhost:${++port}`);
374-
375-
ws.on('open', () => assert.fail(null, null, 'connect shouldnt be raised here'));
376-
ws.on('error', () => done());
377-
});
378-
379308
it('invalid server key is denied', function (done) {
380309
server.createServer(++port, server.handlers.invalidKey, (srv) => {
381310
const ws = new WebSocket(`ws://localhost:${port}`);
@@ -956,7 +885,50 @@ describe('WebSocket', function () {
956885
});
957886

958887
describe('#close', function () {
959-
it('without invalid first argument throws exception', function (done) {
888+
it('closes the connection if called while connecting (1/2)', function (done) {
889+
const wss = new WebSocketServer({ port: ++port }, () => {
890+
const ws = new WebSocket(`ws://localhost:${port}`);
891+
892+
ws.on('open', () => assert.fail(null, null, 'connect shouldnt be raised here'));
893+
ws.on('error', (err) => {
894+
assert.ok(err instanceof Error);
895+
assert.strictEqual(err.message, 'closed before the connection is established');
896+
ws.on('close', () => wss.close(done));
897+
});
898+
ws.close(1001);
899+
});
900+
});
901+
902+
it('closes the connection if called while connecting (2/2)', function (done) {
903+
const wss = new WebSocketServer({
904+
verifyClient: (info, cb) => setTimeout(cb, 300, true),
905+
port: ++port
906+
}, () => {
907+
const ws = new WebSocket(`ws://localhost:${port}`);
908+
909+
ws.on('open', () => assert.fail(null, null, 'connect shouldnt be raised here'));
910+
ws.on('error', (err) => {
911+
assert.ok(err instanceof Error);
912+
assert.strictEqual(err.message, 'closed before the connection is established');
913+
ws.on('close', () => wss.close(done));
914+
});
915+
setTimeout(() => ws.close(1001), 150);
916+
});
917+
});
918+
919+
it('can be called from an error listener while connecting', function (done) {
920+
const ws = new WebSocket(`ws://localhost:${++port}`);
921+
922+
ws.on('open', () => assert.fail(null, null, 'connect shouldnt be raised here'));
923+
ws.on('error', (err) => {
924+
assert.ok(err instanceof Error);
925+
assert.strictEqual(err.code, 'ECONNREFUSED');
926+
ws.close();
927+
ws.on('close', () => done());
928+
});
929+
});
930+
931+
it('throws an error if the first argument is invalid (1/2)', function (done) {
960932
server.createServer(++port, (srv) => {
961933
const ws = new WebSocket(`ws://localhost:${port}`);
962934

@@ -971,7 +943,7 @@ describe('WebSocket', function () {
971943
});
972944
});
973945

974-
it('without reserved error code 1004 throws exception', function (done) {
946+
it('throws an error if the first argument is invalid (2/2)', function (done) {
975947
server.createServer(++port, (srv) => {
976948
const ws = new WebSocket(`ws://localhost:${port}`);
977949

@@ -986,7 +958,7 @@ describe('WebSocket', function () {
986958
});
987959
});
988960

989-
it('without message is successfully transmitted to the server', function (done) {
961+
it('works when close reason is not specified', function (done) {
990962
server.createServer(++port, (srv) => {
991963
const ws = new WebSocket(`ws://localhost:${port}`);
992964

@@ -1000,22 +972,7 @@ describe('WebSocket', function () {
1000972
});
1001973
});
1002974

1003-
it('with message is successfully transmitted to the server', function (done) {
1004-
server.createServer(++port, (srv) => {
1005-
const ws = new WebSocket(`ws://localhost:${port}`);
1006-
1007-
ws.on('open', () => ws.close(1000, 'some reason'));
1008-
1009-
srv.on('close', (code, message, flags) => {
1010-
assert.ok(flags.masked);
1011-
assert.strictEqual(message, 'some reason');
1012-
srv.close(done);
1013-
ws.terminate();
1014-
});
1015-
});
1016-
});
1017-
1018-
it('with encoded message is successfully transmitted to the server', function (done) {
975+
it('works when close reason is specified', function (done) {
1019976
server.createServer(++port, (srv) => {
1020977
const ws = new WebSocket(`ws://localhost:${port}`);
1021978

@@ -1119,6 +1076,51 @@ describe('WebSocket', function () {
11191076
});
11201077
});
11211078

1079+
describe('#terminate', function () {
1080+
it('closes the connection if called while connecting (1/2)', function (done) {
1081+
const wss = new WebSocketServer({ port: ++port }, () => {
1082+
const ws = new WebSocket(`ws://localhost:${port}`);
1083+
1084+
ws.on('open', () => assert.fail(null, null, 'connect shouldnt be raised here'));
1085+
ws.on('error', (err) => {
1086+
assert.ok(err instanceof Error);
1087+
assert.strictEqual(err.message, 'closed before the connection is established');
1088+
ws.on('close', () => wss.close(done));
1089+
});
1090+
ws.terminate();
1091+
});
1092+
});
1093+
1094+
it('closes the connection if called while connecting (2/2)', function (done) {
1095+
const wss = new WebSocketServer({
1096+
verifyClient: (info, cb) => setTimeout(cb, 300, true),
1097+
port: ++port
1098+
}, () => {
1099+
const ws = new WebSocket(`ws://localhost:${port}`);
1100+
1101+
ws.on('open', () => assert.fail(null, null, 'connect shouldnt be raised here'));
1102+
ws.on('error', (err) => {
1103+
assert.ok(err instanceof Error);
1104+
assert.strictEqual(err.message, 'closed before the connection is established');
1105+
ws.on('close', () => wss.close(done));
1106+
});
1107+
setTimeout(() => ws.terminate(), 150);
1108+
});
1109+
});
1110+
1111+
it('can be called from an error listener while connecting', function (done) {
1112+
const ws = new WebSocket(`ws://localhost:${++port}`);
1113+
1114+
ws.on('open', () => assert.fail(null, null, 'connect shouldnt be raised here'));
1115+
ws.on('error', (err) => {
1116+
assert.ok(err instanceof Error);
1117+
assert.strictEqual(err.code, 'ECONNREFUSED');
1118+
ws.terminate();
1119+
ws.on('close', () => done());
1120+
});
1121+
});
1122+
});
1123+
11221124
describe('WHATWG API emulation', function () {
11231125
it('should not throw errors when getting and setting', function () {
11241126
const listener = () => {};

0 commit comments

Comments
 (0)