The wsPreEncoded option was added in the `socket.io-adapter` package
when broadcasting a message to multiple clients.
It was removed in [1] and is now superseded by the `wsPreEncodedFrame`
option, which directly computes the WebSocket frame once for all
clients (see [2]).
[1]: 88eee5948a
[2]: 5f7b47d40f
With the `websocket` transport, the callbacks which indicate that the
packets are actually written were not properly called.
Example:
```js
socket.send("hello", () => {
// the message has been written to the underlying transport
});
```
The bug was caused by the `websocket` transport (and `webtransport` as
well) having its `supportsFraming` property set to `true`, despite
having been changed in [1] to emit a single `drain` event for each
batch of messages written to the transport like the `polling` transport
always did. Note that although [1] is partially reverted in [2], the
new `drain` event behavior is preserved as called out in that commit's
message.
The `supportsFraming` attribute was introduced in [3] (amended by [4])
as a way to distinguish transports that emit one `drain` per message
from those that emit one `drain` per batch. Since the delivery of
`send` callbacks depends on matching `drain` events with
`transport.send` calls, that distinction is vital to correct behavior.
However, now that all transports have converged to "one `drain` per
batch" behavior, this `supportsFraming` property can be retired (and
the code for calling callbacks simplified).
[1]: https://github.com/socketio/engine.io/pull/618
[2]: a65a047526
[3]: https://github.com/socketio/engine.io/pull/130
[4]: https://github.com/socketio/engine.io/pull/132
Related: https://github.com/socketio/engine.io/issues/698
A reference to the initial IncomingMessage object (the first HTTP
request of the session) is kept in memory by default (`socket.request`),
so its attached ServerResponse object (`req.res`) would not be
garbage-collected. This will now be the case.
Note: the IncomingMessage object is needed in two cases:
- when working with the `express-session` middleware (`request.session`)
- when fetching the certificate of the client with `request.socket.getPeerCertificate()`
That's why removing it would be a breaking change.
This header should not be needed since the client already includes a
cache busting query parameter ("t"), but a misconfigured CDN could
ignore the query parameters and cache the server response.
Related: https://github.com/socketio/socket.io/issues/4842
Refreshing the page with a client connected with WebTransport would
trigger the following exception:
> node:internal/process/promises:288
> triggerUncaughtException(err, true /* fromPromise */);
> ^
>
> [UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "0".] {
> code: 'ERR_UNHANDLED_REJECTION'
> }
Related: https://github.com/socketio/engine.io/issues/688
This bug only exists for polling transport connections running on top
of uWS.
If the remote client abruptly disconnects (thus aborting the request)
while the server is waiting on an asynchronous operation such as
compression, the server may attempt to write a response via the aborted
response object. This causes an uncaught exception to be thrown.
A specially crafted request could lead to the following exception:
> TypeError: Cannot read properties of undefined (reading 'handlesUpgrades')
> at Server.onWebSocket (build/server.js:515:67)
This bug was introduced in [1], released in version 5.1.0 and included
in version 4.1.0 of the `socket.io` parent package. Older versions are
not impacted.
[1]: 7096e98a02
In order to prevent issues like:
> error TS2345: Argument of type 'RequestHandler<ParamsDictionary, any, any, ParsedQs, Record<string, any>>' is not assignable to parameter of type 'Middleware'.
> Types of parameters 'req' and 'req' are incompatible.
> Type 'IncomingMessage' is missing the following properties from type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>': get, header, accepts, acceptsCharsets, and 29 more.
>
> io.engine.use(sessionMiddleware);
~~~~~~~~~~~~~~~~~
Related: https://github.com/socketio/socket.io/issues/4644
We could also have use the RequestHandler type from the
@types/express-serve-static-core package, but that would add 5 new
dependencies.
See also: https://github.com/socketio/engine.io/issues/673
The class used to accumulate the response headers did not expose the
exact same API as its wrapped type, which could lead to the following
error in some rare cases:
> TypeError: Cannot read properties of undefined (reading 'end')
> at Polling.onDataRequest (build/transports-uws/polling.js:109:53)
> at Polling.onRequest (build/transports-uws/polling.js:47:18)
> at callback (build/userver.js:94:56)
> at uServer.verify (build/server.js:152:9)
Related: https://github.com/socketio/socket.io/issues/4643
This commit implements middlewares at the Engine.IO level, because
Socket.IO middlewares are meant for namespace authorization and are not
executed during a classic HTTP request/response cycle.
A workaround was possible by using the allowRequest option and the
"headers" event, but this feels way cleaner and works with upgrade
requests too.
Syntax:
```js
engine.use((req, res, next) => {
// do something
next();
});
// with express-session
import session from "express-session";
engine.use(session({
secret: "keyboard cat",
resave: false,
saveUninitialized: true,
cookie: { secure: true }
});
// with helmet
import helmet from "helmet";
engine.use(helmet());
```
Related:
- https://github.com/socketio/engine.io/issues/668
- https://github.com/socketio/engine.io/issues/651
- https://github.com/socketio/socket.io/issues/4609
- https://github.com/socketio/socket.io/issues/3933
- a lot of other issues asking for compatibility with express-session
This reverts commit [1], which was included in `engine.io@5.1.0` and
`socket.io@4.1.0`.
The WebSocket connection was closed before all packets were written
out, so for example when calling `socket.disconnect(true)` on the
Socket.IO server (which disconnect from all namespaces and close the
connection), the client would receive only the first disconnect packet
and kept trying to reconnect to the other namespaces.
The only difference with the previous implementation (pre 5.1.0) is
that the "drain" event gets only called once at the end, and not after
each packet.
[1]: ad5306aeae
Related: https://github.com/socketio/engine.io/issues/648
This major bump creates a lot of noise, but it is necessary for
prettier to be able to parse new syntax such as:
- typed imports: `import { type xxx } from ...`
- private attributes: `class A { #b; #c() {} }`
The "addTrailingSlash" option allows to control whether a trailing
slash is added to the path of the HTTP requests:
- true (default): "/engine.io/"
- false: "/engine.io"
Related: 21a6e1219a
Signed-off-by: iifawzi <iifawzie@gmail.com>
This optimization is only applied if:
- the permessage-deflate extension is disabled (which is the default)
- the "ws" package is used (which is the default)
In that case, the WebSocket frame will only be computed once, when
broadcasting to multiple clients.
Related: 5f7b47d40f
Before this change, receiving an HTTP2 upgrade would make the server
crash:
> Error: read ECONNRESET
> at TCP.onStreamRead (node:internal/stream_base_commons:217:20) {
> errno: -104,
> code: 'ECONNRESET',
> syscall: 'read'
> }
This can be reproduced with Node.js v14.15.3, v16.18.1 and v18.12.1.
So that clients in HTTP long-polling can decide how many packets they
have to send to stay under the maxHttpBufferSize value.
This is a backward compatible change which should not mandate a new
major revision of the protocol (we stay in v4), as we only add a field
in the JSON-encoded handshake data:
```
0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000,"maxPayload":1000000}
```
Related: https://github.com/socketio/socket.io-client/issues/1531
With the engine based on µWebSockets.js (introduced in version 6.1.0),
a huge request body split in multiple chunks would throw the following
error:
> node:buffer:254
> TypedArrayPrototypeSet(target, source, targetStart);
> ^
>
> TypeError: Cannot perform %TypedArray%.prototype.set on a detached ArrayBuffer
> at Buffer.set (<anonymous>)
> at _copyActual (node:buffer:254:3)
> node:buffer:254
> TypedArrayPrototypeSet(target, source, targetStart);
> ^
>
> TypeError: Cannot perform %TypedArray%.prototype.set on a detached ArrayBuffer
> at Buffer.set (<anonymous>)
> at _copyActual (node:buffer:254:3)
> at Function.concat (node:buffer:562:12)
> at onEnd (.../node_modules/engine.io/build/transports-uws/polling.js:126:32)
> at .../node_modules/engine.io/build/transports-uws/polling.js:143:17
Note: µWebSockets.js does not currently support chunked transfer
encoding.
When binding to an uWebSockets.js application, the server could crash
with the following error:
```
TypeError: res.onData is not a function
at Polling.onDataRequest (build/transports-uws/polling.js:133:13)
at Polling.onRequest (build/transports-uws/polling.js:47:18)
at callback (build/userver.js:80:56)
```
Related: https://github.com/socketio/engine.io/issues/637
When binding to an uWebSockets.js App, there was an unhandled case that
could crash the server:
```
curl "http://localhost:3000/engine.io/?EIO=4&transport=websocket"
```
would result in:
```
Error: Returning from a request handler without responding or attaching an abort handler is forbidden!
terminate called without an active exception
```
Note: this does not apply to the default server based on ws, because
the error was caught elsewhere in the source code.
Related: https://github.com/socketio/socket.io/issues/4250
**IMPORTANT SECURITY FIX**
A malicious client could send a specially crafted HTTP request,
triggering an uncaught exception and killing the Node.js process:
> RangeError: Invalid WebSocket frame: RSV2 and RSV3 must be clear
> at Receiver.getInfo (/.../node_modules/ws/lib/receiver.js:176:14)
> at Receiver.startLoop (/.../node_modules/ws/lib/receiver.js:136:22)
> at Receiver._write (/.../node_modules/ws/lib/receiver.js:83:10)
> at writeOrBuffer (internal/streams/writable.js:358:12)
This bug was introduced by [1], included in `engine.io@4.0.0`, so
previous releases are not impacted.
[1]: f3c291fa61
Thanks to Marcus Wejderot from Mevisio for the responsible disclosure.
The v3 parser (used for compatibility with older clients) was broken
during the migration to TypeScript ([1]).
This was not caught in the test suite because the Node.js client does
not support binary packet in polling mode (packets are base64-encoded).
[1]: c0d6eaa1ba
Backported from 6.0.x branch: 3f42262fd2
This should fix the following error:
> TypeError: Cannot read property 'writeHead' of undefined
This bug was introduced by [1], because the `if (res !== undefined) { ... }`
check was removed.
But `res` is indeed undefined when the client connects with WebSocket
directly, in that case we need to manually write the response content
(in the abortUpgrade method).
Please note that the previous behavior was invalid too, since the
WebSocket connection was left open when an error occurred during the
handshake.
[1]: 7096e98a02