Skip to content

Commit 2505e42

Browse files
authored
websocket: improve .close() (#2865)
1 parent 1216ba0 commit 2505e42

File tree

6 files changed

+69
-13
lines changed

6 files changed

+69
-13
lines changed

lib/web/websocket/connection.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict'
22

3-
const { uid, states } = require('./constants')
3+
const { uid, states, sentCloseFrameState } = require('./constants')
44
const {
55
kReadyState,
66
kSentClose,
@@ -230,7 +230,7 @@ function onSocketClose () {
230230
// If the TCP connection was closed after the
231231
// WebSocket closing handshake was completed, the WebSocket connection
232232
// is said to have been closed _cleanly_.
233-
const wasClean = ws[kSentClose] && ws[kReceivedClose]
233+
const wasClean = ws[kSentClose] === sentCloseFrameState.SENT && ws[kReceivedClose]
234234

235235
let code = 1005
236236
let reason = ''
@@ -240,7 +240,7 @@ function onSocketClose () {
240240
if (result) {
241241
code = result.code ?? 1005
242242
reason = result.reason
243-
} else if (!ws[kSentClose]) {
243+
} else if (ws[kSentClose] !== sentCloseFrameState.SENT) {
244244
// If _The WebSocket
245245
// Connection is Closed_ and no Close control frame was received by the
246246
// endpoint (such as could occur if the underlying transport connection

lib/web/websocket/constants.js

+7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ const states = {
2020
CLOSED: 3
2121
}
2222

23+
const sentCloseFrameState = {
24+
NOT_SENT: 0,
25+
PROCESSING: 1,
26+
SENT: 2
27+
}
28+
2329
const opcodes = {
2430
CONTINUATION: 0x0,
2531
TEXT: 0x1,
@@ -42,6 +48,7 @@ const emptyBuffer = Buffer.allocUnsafe(0)
4248

4349
module.exports = {
4450
uid,
51+
sentCloseFrameState,
4552
staticPropertyDescriptors,
4653
states,
4754
opcodes,

lib/web/websocket/receiver.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict'
22

33
const { Writable } = require('node:stream')
4-
const { parserStates, opcodes, states, emptyBuffer } = require('./constants')
4+
const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants')
55
const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols')
66
const { channels } = require('../../core/diagnostics')
77
const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived } = require('./util')
@@ -104,7 +104,7 @@ class ByteParser extends Writable {
104104

105105
this.#info.closeInfo = this.parseCloseBody(body)
106106

107-
if (!this.ws[kSentClose]) {
107+
if (this.ws[kSentClose] !== sentCloseFrameState.SENT) {
108108
// If an endpoint receives a Close frame and did not previously send a
109109
// Close frame, the endpoint MUST send a Close frame in response. (When
110110
// sending a Close frame in response, the endpoint typically echos the
@@ -120,7 +120,7 @@ class ByteParser extends Writable {
120120
closeFrame.createFrame(opcodes.CLOSE),
121121
(err) => {
122122
if (!err) {
123-
this.ws[kSentClose] = true
123+
this.ws[kSentClose] = sentCloseFrameState.SENT
124124
}
125125
}
126126
)

lib/web/websocket/util.js

+14
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ const { MessageEvent, ErrorEvent } = require('./events')
88

99
/**
1010
* @param {import('./websocket').WebSocket} ws
11+
* @returns {boolean}
12+
*/
13+
function isConnecting (ws) {
14+
// If the WebSocket connection is not yet established, and the connection
15+
// is not yet closed, then the WebSocket connection is in the CONNECTING state.
16+
return ws[kReadyState] === states.CONNECTING
17+
}
18+
19+
/**
20+
* @param {import('./websocket').WebSocket} ws
21+
* @returns {boolean}
1122
*/
1223
function isEstablished (ws) {
1324
// If the server's response is validated as provided for above, it is
@@ -18,6 +29,7 @@ function isEstablished (ws) {
1829

1930
/**
2031
* @param {import('./websocket').WebSocket} ws
32+
* @returns {boolean}
2133
*/
2234
function isClosing (ws) {
2335
// Upon either sending or receiving a Close control frame, it is said
@@ -28,6 +40,7 @@ function isClosing (ws) {
2840

2941
/**
3042
* @param {import('./websocket').WebSocket} ws
43+
* @returns {boolean}
3144
*/
3245
function isClosed (ws) {
3346
return ws[kReadyState] === states.CLOSED
@@ -190,6 +203,7 @@ function failWebsocketConnection (ws, reason) {
190203
}
191204

192205
module.exports = {
206+
isConnecting,
193207
isEstablished,
194208
isClosing,
195209
isClosed,

lib/web/websocket/websocket.js

+18-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const { webidl } = require('../fetch/webidl')
44
const { URLSerializer } = require('../fetch/data-url')
55
const { getGlobalOrigin } = require('../fetch/global')
6-
const { staticPropertyDescriptors, states, opcodes, emptyBuffer } = require('./constants')
6+
const { staticPropertyDescriptors, states, sentCloseFrameState, opcodes, emptyBuffer } = require('./constants')
77
const {
88
kWebSocketURL,
99
kReadyState,
@@ -13,7 +13,15 @@ const {
1313
kSentClose,
1414
kByteParser
1515
} = require('./symbols')
16-
const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection, fireEvent } = require('./util')
16+
const {
17+
isConnecting,
18+
isEstablished,
19+
isClosed,
20+
isClosing,
21+
isValidSubprotocol,
22+
failWebsocketConnection,
23+
fireEvent
24+
} = require('./util')
1725
const { establishWebSocketConnection } = require('./connection')
1826
const { WebsocketFrameSend } = require('./frame')
1927
const { ByteParser } = require('./receiver')
@@ -132,6 +140,8 @@ class WebSocket extends EventTarget {
132140
// be CONNECTING (0).
133141
this[kReadyState] = WebSocket.CONNECTING
134142

143+
this[kSentClose] = sentCloseFrameState.NOT_SENT
144+
135145
// The extensions attribute must initially return the empty string.
136146

137147
// The protocol attribute must initially return the empty string.
@@ -184,7 +194,7 @@ class WebSocket extends EventTarget {
184194
}
185195

186196
// 3. Run the first matching steps from the following list:
187-
if (this[kReadyState] === WebSocket.CLOSING || this[kReadyState] === WebSocket.CLOSED) {
197+
if (isClosing(this) || isClosed(this)) {
188198
// If this's ready state is CLOSING (2) or CLOSED (3)
189199
// Do nothing.
190200
} else if (!isEstablished(this)) {
@@ -193,7 +203,7 @@ class WebSocket extends EventTarget {
193203
// to CLOSING (2).
194204
failWebsocketConnection(this, 'Connection was closed before it was established.')
195205
this[kReadyState] = WebSocket.CLOSING
196-
} else if (!isClosing(this)) {
206+
} else if (this[kSentClose] === sentCloseFrameState.NOT_SENT) {
197207
// If the WebSocket closing handshake has not yet been started
198208
// Start the WebSocket closing handshake and set this's ready
199209
// state to CLOSING (2).
@@ -204,6 +214,8 @@ class WebSocket extends EventTarget {
204214
// - If reason is also present, then reasonBytes must be
205215
// provided in the Close message after the status code.
206216

217+
this[kSentClose] = sentCloseFrameState.PROCESSING
218+
207219
const frame = new WebsocketFrameSend()
208220

209221
// If neither code nor reason is present, the WebSocket Close
@@ -230,7 +242,7 @@ class WebSocket extends EventTarget {
230242

231243
socket.write(frame.createFrame(opcodes.CLOSE), (err) => {
232244
if (!err) {
233-
this[kSentClose] = true
245+
this[kSentClose] = sentCloseFrameState.SENT
234246
}
235247
})
236248

@@ -258,7 +270,7 @@ class WebSocket extends EventTarget {
258270

259271
// 1. If this's ready state is CONNECTING, then throw an
260272
// "InvalidStateError" DOMException.
261-
if (this[kReadyState] === WebSocket.CONNECTING) {
273+
if (isConnecting(this)) {
262274
throw new DOMException('Sent before connected.', 'InvalidStateError')
263275
}
264276

test/websocket/close.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict'
22

3-
const { describe, test } = require('node:test')
3+
const { tspl } = require('@matteo.collina/tspl')
4+
const { describe, test, after } = require('node:test')
45
const assert = require('node:assert')
56
const { WebSocketServer } = require('ws')
67
const { WebSocket } = require('../..')
@@ -128,4 +129,26 @@ describe('Close', () => {
128129
ws.addEventListener('open', () => ws.close(3000))
129130
})
130131
})
132+
133+
test('calling close twice will only trigger the close event once', async (t) => {
134+
t = tspl(t, { plan: 1 })
135+
136+
const server = new WebSocketServer({ port: 0 })
137+
138+
after(() => server.close())
139+
140+
server.on('connection', (ws) => {
141+
ws.on('close', (code) => {
142+
t.strictEqual(code, 1000)
143+
})
144+
})
145+
146+
const ws = new WebSocket(`ws://localhost:${server.address().port}`)
147+
ws.addEventListener('open', () => {
148+
ws.close(1000)
149+
ws.close(1000)
150+
})
151+
152+
await t.completed
153+
})
131154
})

0 commit comments

Comments
 (0)