Skip to content

Commit 7e12397

Browse files
KhafraDevjodevsa
andauthored
feat: implement websockets (nodejs#1795)
* initial handshake * minor fixes * feat: working initial handshake! * feat(ws): initial WebSocket class implementation * fix: allow http: and ws: urls * fix(ws): use websocket spec * fix(ws): use websocket spec * feat: implement url getter * feat: implement some of `WebSocket.close` and ready state * fix: body is null for websockets & pass socket to response * fix: store the fetch controller & response on ws * fix: remove invalid tests * feat: implement readyState getter * feat: implement `protocol` and `extensions` getters * feat: implement event listeners * feat: implement binaryType attribute * fix: add argument length checks * feat: basic unfragmented message parsing * fix: always remove previous listener * feat: add in idlharness WPT * implement sending a message for WS and add a websocketFrame class * feat: allow sending ArrayBuffer/Views & Blob * fix: remove duplicate `upgrade` and `connection` headers * feat: add in WebSocket.close() and handle closing frames * refactor WebsocketFrame and support receiving frames in multiple chunks * fixes * move WebsocketFrame to its own file * feat: export WebSocket & add types * fix: tsd * feat(wpt): use WebSocketServer & run test * fix: properly set/read close code & close reason * fix: flakiness in websocket test runner * fix: receive message with arraybuffer binary type * feat: split WebsocketFrame into 2 classes (sent & received) * fix: parse fragmented frames more efficiently & close frame * fix: add types for MessageEvent and CloseEvent * fix: subprotocol validation & add wpts * fix: protocol validation & protocol webidl & add wpts * fix: correct bufferedAmount calc. & message event w/ blob * fix: don't truncate typedarrays * feat: add remaining wpts * fix: allow sending payloads > 65k bytes * fix: mask data > 125 bytes properly * revert changes to core * fix: decrement bufferedAmount after write * fix: handle ping and pong frames * fix: simplify receiving frame logic * fix: disable extensions & validate frames * fix: send close frame upon receiving * lint * fix: validate status code & utf-8 * fix: add hooks * fix: check if frame is unfragmented correctly * fix: send ping app data in pong frames * export websocket on node >= 18 & add diagnostic_channels * mark test as flaky * fix: couple bug fixes * fix: fragmented frame end detection * fix: use TextDecoder for utf-8 validation * fix: handle incomplete chunks * revert: handle incomplete chunks * mark WebSockets as experimental * fix: sending 65k bytes is still flaky on linux * fix: apply suggestions * fix: apply some suggestions * add basic docs * feat: use streaming parser for frames * feat: validate some frames & remove WebsocketFrame class * fix: parse close frame & move failWebsocketConnection * fix: read close reason and read entire close body * fix: echo close frame if one hasn't been sent * fix: emit message event on message receive * fix: minor fixes * fix: ci * fix: set was clean exit after server receives close frame * fix: check if received close frame for clean close * fix: set sent close after writing frame * feat: implement error messages * fix: add error event handler to socket * fix: address reviews Co-authored-by: Subhi Al Hasan <[email protected]>
1 parent f6323f6 commit 7e12397

File tree

92 files changed

+3881
-15
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+3881
-15
lines changed

docs/api/DiagnosticsChannel.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,44 @@ diagnosticsChannel.channel('undici:client:connectError').subscribe(({ error, soc
135135
// connector is a function that creates the socket
136136
console.log(`Connect failed with ${error.message}`)
137137
})
138+
```
139+
140+
## `undici:websocket:open`
141+
142+
This message is published after the client has successfully connected to a server.
143+
144+
```js
145+
import diagnosticsChannel from 'diagnostics_channel'
146+
147+
diagnosticsChannel.channel('undici:websocket:open').subscribe(({ address, protocol, extensions }) => {
148+
console.log(address) // address, family, and port
149+
console.log(protocol) // negotiated subprotocols
150+
console.log(extensions) // negotiated extensions
151+
})
152+
```
153+
154+
## `undici:websocket:close`
155+
156+
This message is published after the connection has closed.
157+
158+
```js
159+
import diagnosticsChannel from 'diagnostics_channel'
160+
161+
diagnosticsChannel.channel('undici:websocket:close').subscribe(({ websocket, code, reason }) => {
162+
console.log(websocket) // the WebSocket object
163+
console.log(code) // the closing status code
164+
console.log(reason) // the closing reason
165+
})
166+
```
167+
168+
## `undici:websocket:socket_error`
169+
170+
This message is published if the socket experiences an error.
171+
172+
```js
173+
import diagnosticsChannel from 'diagnostics_channel'
174+
175+
diagnosticsChannel.channel('undici:websocket:socket_error').subscribe((error) => {
176+
console.log(error)
177+
})
178+
```

docs/api/WebSocket.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Class: WebSocket
2+
3+
> ⚠️ Warning: the WebSocket API is experimental and has known bugs.
4+
5+
Extends: [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)
6+
7+
The WebSocket object provides a way to manage a WebSocket connection to a server, allowing bidirectional communication. The API follows the [WebSocket spec](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket).
8+
9+
## `new WebSocket(url[, protocol])`
10+
11+
Arguments:
12+
13+
* **url** `URL | string` - The url's protocol *must* be `ws` or `wss`.
14+
* **protocol** `string | string[]` (optional) - Subprotocol(s) to request the server use.
15+
16+
## Read More
17+
18+
- [MDN - WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
19+
- [The WebSocket Specification](https://www.rfc-editor.org/rfc/rfc6455)
20+
- [The WHATWG WebSocket Specification](https://websockets.spec.whatwg.org/)

docsify/sidebar.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* [MockErrors](/docs/api/MockErrors.md "Undici API - MockErrors")
1717
* [API Lifecycle](/docs/api/api-lifecycle.md "Undici API - Lifecycle")
1818
* [Diagnostics Channel Support](/docs/api/DiagnosticsChannel.md "Diagnostics Channel Support")
19+
* [WebSocket](/docs/api/WebSocket.md "Undici API - WebSocket")
1920
* Best Practices
2021
* [Proxy](/docs/best-practices/proxy.md "Connecting through a proxy")
2122
* [Client Certificate](/docs/best-practices/client-certificate.md "Connect using a client certificate")

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './types/file'
2121
export * from './types/filereader'
2222
export * from './types/formdata'
2323
export * from './types/diagnostics-channel'
24+
export * from './types/websocket'
2425
export { Interceptable } from './types/mock-interceptor'
2526

2627
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler }

index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) {
119119
module.exports.getGlobalOrigin = getGlobalOrigin
120120
}
121121

122+
if (nodeMajor >= 18) {
123+
const { WebSocket } = require('./lib/websocket/websocket')
124+
125+
module.exports.WebSocket = WebSocket
126+
}
127+
122128
module.exports.request = makeDispatcher(api.request)
123129
module.exports.stream = makeDispatcher(api.stream)
124130
module.exports.pipeline = makeDispatcher(api.pipeline)

lib/fetch/index.js

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const { dataURLProcessor, serializeAMimeType } = require('./dataURL')
5858
const { TransformStream } = require('stream/web')
5959
const { getGlobalDispatcher } = require('../../index')
6060
const { webidl } = require('./webidl')
61+
const { STATUS_CODES } = require('http')
6162

6263
/** @type {import('buffer').resolveObjectURL} */
6364
let resolveObjectURL
@@ -1745,12 +1746,17 @@ async function httpNetworkFetch (
17451746
}
17461747

17471748
try {
1748-
const { body, status, statusText, headersList } = await dispatch({ body: requestBody })
1749+
// socket is only provided for websockets
1750+
const { body, status, statusText, headersList, socket } = await dispatch({ body: requestBody })
17491751

1750-
const iterator = body[Symbol.asyncIterator]()
1751-
fetchParams.controller.next = () => iterator.next()
1752+
if (socket) {
1753+
response = makeResponse({ status, statusText, headersList, socket })
1754+
} else {
1755+
const iterator = body[Symbol.asyncIterator]()
1756+
fetchParams.controller.next = () => iterator.next()
17521757

1753-
response = makeResponse({ status, statusText, headersList })
1758+
response = makeResponse({ status, statusText, headersList })
1759+
}
17541760
} catch (err) {
17551761
// 10. If aborted, then:
17561762
if (err.name === 'AbortError') {
@@ -1934,7 +1940,10 @@ async function httpNetworkFetch (
19341940

19351941
async function dispatch ({ body }) {
19361942
const url = requestCurrentURL(request)
1937-
return new Promise((resolve, reject) => fetchParams.controller.dispatcher.dispatch(
1943+
/** @type {import('../..').Agent} */
1944+
const agent = fetchParams.controller.dispatcher
1945+
1946+
return new Promise((resolve, reject) => agent.dispatch(
19381947
{
19391948
path: url.pathname + url.search,
19401949
origin: url.origin,
@@ -1943,7 +1952,8 @@ async function httpNetworkFetch (
19431952
headers: request.headersList[kHeadersCaseInsensitive],
19441953
maxRedirections: 0,
19451954
bodyTimeout: 300_000,
1946-
headersTimeout: 300_000
1955+
headersTimeout: 300_000,
1956+
upgrade: request.mode === 'websocket' ? 'websocket' : undefined
19471957
},
19481958
{
19491959
body: null,
@@ -2062,6 +2072,30 @@ async function httpNetworkFetch (
20622072
fetchParams.controller.terminate(error)
20632073

20642074
reject(error)
2075+
},
2076+
2077+
onUpgrade (status, headersList, socket) {
2078+
if (status !== 101) {
2079+
return
2080+
}
2081+
2082+
const headers = new Headers()
2083+
2084+
for (let n = 0; n < headersList.length; n += 2) {
2085+
const key = headersList[n + 0].toString('latin1')
2086+
const val = headersList[n + 1].toString('latin1')
2087+
2088+
headers.append(key, val)
2089+
}
2090+
2091+
resolve({
2092+
status,
2093+
statusText: STATUS_CODES[status],
2094+
headersList: headers[kHeadersList],
2095+
socket
2096+
})
2097+
2098+
return true
20652099
}
20662100
}
20672101
))

lib/fetch/webidl.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,10 +472,20 @@ webidl.converters['unsigned long long'] = function (V) {
472472
return x
473473
}
474474

475+
// https://webidl.spec.whatwg.org/#es-unsigned-long
476+
webidl.converters['unsigned long'] = function (V) {
477+
// 1. Let x be ? ConvertToInt(V, 32, "unsigned").
478+
const x = webidl.util.ConvertToInt(V, 32, 'unsigned')
479+
480+
// 2. Return the IDL unsigned long value that
481+
// represents the same numeric value as x.
482+
return x
483+
}
484+
475485
// https://webidl.spec.whatwg.org/#es-unsigned-short
476-
webidl.converters['unsigned short'] = function (V) {
486+
webidl.converters['unsigned short'] = function (V, opts) {
477487
// 1. Let x be ? ConvertToInt(V, 16, "unsigned").
478-
const x = webidl.util.ConvertToInt(V, 16, 'unsigned')
488+
const x = webidl.util.ConvertToInt(V, 16, 'unsigned', opts)
479489

480490
// 2. Return the IDL unsigned short value that represents
481491
// the same numeric value as x.

0 commit comments

Comments
 (0)