Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions lib/web/websocket/stream/websocketstream.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,12 +365,15 @@ class WebSocketStream {
return
}

// 3. If stream s was ever connected is false, then reject stream s opened promise with a new WebSocketError.
// 3. If stream 's was ever connected is false, then reject stream 's opened promise with a new WebSocketError.
if (!this.#handler.wasEverConnected) {
this.#openedPromise.reject(new WebSocketError('Socket never opened'))
const error = new WebSocketError('Socket never opened')
this.#openedPromise.reject(error)
this.#closedPromise.reject(error)
return
}

const result = this.#parser.closingInfo
const result = this.#parser?.closingInfo

// 4. Let code be the WebSocket connection close code .
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
Expand All @@ -391,15 +394,15 @@ class WebSocketStream {

// 6. If the connection was closed cleanly ,
if (wasClean) {
// 6.1. Close stream s readable stream .
// 6.1. Close stream 's readable stream .
this.#readableStreamController.close()

// 6.2. Error stream s writable stream with an " InvalidStateError " DOMException indicating that a closed WebSocketStream cannot be written to.
// 6.2. Error stream 's writable stream with an " InvalidStateError " DOMException indicating that a closed WebSocketStream cannot be written to.
if (!this.#writableStream.locked) {
this.#writableStream.abort(new DOMException('A closed WebSocketStream cannot be written to', 'InvalidStateError'))
}

// 6.3. Resolve stream s closed promise with WebSocketCloseInfo «[ " closeCode " → code , " reason " → reason ]».
// 6.3. Resolve stream 's closed promise with WebSocketCloseInfo «[ " closeCode " → code , " reason " → reason ]».
this.#closedPromise.resolve({
closeCode: code,
reason
Expand All @@ -410,13 +413,13 @@ class WebSocketStream {
// 7.1. Let error be a new WebSocketError whose closeCode is code and reason is reason .
const error = createUnvalidatedWebSocketError('unclean close', code, reason)

// 7.2. Error stream s readable stream with error .
// 7.2. Error stream 's readable stream with error .
this.#readableStreamController.error(error)

// 7.3. Error stream s writable stream with error .
// 7.3. Error stream 's writable stream with error .
this.#writableStream.abort(error)

// 7.4. Reject stream s closed promise with error .
// 7.4. Reject stream 's closed promise with error .
this.#closedPromise.reject(error)
}
}
Expand Down
169 changes: 156 additions & 13 deletions test/web-platform-tests/expectation.json
Original file line number Diff line number Diff line change
Expand Up @@ -33082,7 +33082,8 @@
"cases": [
{
"name": "Create WebSocket - Close the Connection - close(1000, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed",
"success": true
"success": false,
"message": "assert_true: WebSocket connection should be opened expected true got false"
}
]
},
Expand Down Expand Up @@ -39058,8 +39059,7 @@
},
{
"name": "close during handshake should work",
"success": false,
"message": "Cannot read properties of undefined (reading 'closingInfo')"
"success": true
},
{
"name": "close() with invalid code 999 should throw",
Expand Down Expand Up @@ -39198,8 +39198,7 @@
},
{
"name": "close during handshake should work",
"success": false,
"message": "Cannot read properties of undefined (reading 'closingInfo')"
"success": true
},
{
"name": "close() with invalid code 999 should throw",
Expand Down Expand Up @@ -39299,12 +39298,84 @@
]
},
"constructor.any.html?wpt_flags=h2": {
"success": false,
"cases": []
"success": true,
"cases": [
{
"name": "constructing with no URL should throw",
"success": true
},
{
"name": "constructing with an invalid URL should throw",
"success": true
},
{
"name": "constructing with invalid options should throw",
"success": true
},
{
"name": "protocols should be required to be a list",
"success": true
},
{
"name": "constructing with a valid URL should work",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "setting a protocol in the constructor should work",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "connection failure should reject the promises",
"success": true
},
{
"name": "wss.opened should resolve to the right types",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
}
]
},
"constructor.any.html?wss": {
"success": false,
"cases": []
"success": true,
"cases": [
{
"name": "constructing with no URL should throw",
"success": true
},
{
"name": "constructing with an invalid URL should throw",
"success": true
},
{
"name": "constructing with invalid options should throw",
"success": true
},
{
"name": "protocols should be required to be a list",
"success": true
},
{
"name": "constructing with a valid URL should work",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "setting a protocol in the constructor should work",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "connection failure should reject the promises",
"success": true
},
{
"name": "wss.opened should resolve to the right types",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
}
]
},
"remote-close.any.html?default": {
"success": true,
Expand Down Expand Up @@ -39341,12 +39412,84 @@
]
},
"remote-close.any.html?wpt_flags=h2": {
"success": false,
"cases": []
"success": true,
"cases": [
{
"name": "clean close should be clean",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "close frame with no body should result in status code 1005",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "reason should be passed through",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "UTF-8 reason should work",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "close with unwritten data should not be considered clean",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "remote code and reason should be used",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "abrupt close should give an error",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
}
]
},
"remote-close.any.html?wss": {
"success": false,
"cases": []
"success": true,
"cases": [
{
"name": "clean close should be clean",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "close frame with no body should result in status code 1005",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "reason should be passed through",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "UTF-8 reason should work",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "close with unwritten data should not be considered clean",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "remote code and reason should be used",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
},
{
"name": "abrupt close should give an error",
"success": false,
"message": "promise_test: Unhandled rejection with value: object \"WebSocketError: Socket never opened\""
}
]
},
"websocket-error.any.html": {
"success": true,
Expand Down
27 changes: 27 additions & 0 deletions test/websocket/stream/connection-failure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use strict'

const { test } = require('node:test')
const { WebSocketStream } = require('../../..')
const { createServer } = require('node:http')

// https://github.com/nodejs/undici/issues/4732
test('WebSocketStream rejects opened/closed promises when connection fails', async (t) => {
const server = createServer((req, res) => {
res.writeHead(404)
res.end('Not Found')
})

await new Promise(resolve => server.listen(0, resolve))
t.after(() => server.close())

const wss = new WebSocketStream(`ws://localhost:${server.address().port}`)

await t.assert.rejects(wss.opened, {
name: 'WebSocketError',
message: 'Socket never opened'
})

await t.assert.rejects(wss.closed, {
name: 'WebSocketError'
})
})
Loading