Compare commits

...

17 Commits

Author SHA1 Message Date
Damien Arrachequesne
439a8f669c chore(release): engine.io@6.6.7
Diff: https://github.com/socketio/socket.io/compare/engine.io@6.6.6...engine.io@6.6.7
2026-04-27 11:20:14 +02:00
Damien Arrachequesne
fc11285e14 fix(eio): close HTTP requests with invalid content type
Before this commit, after the initial handshake, an HTTP request with a "application/octet-stream"
content type would trigger a "invalid content" error at the Engine.IO level, but would not be
properly closed, which could possibly lead to resource exhaustion.

This behavior was introduced in [1] (engine.io@4.1.0, January 2021).

[1]: 663d326d18
2026-04-27 11:08:50 +02:00
Damien Arrachequesne
b059af6b12 refactor(eio): use plain IncomingMessage in the public API
The EngineRequest type was introduced in [1] (engine.io@6.6.0).

[1]: c310b7b6b6

Related: https://github.com/socketio/socket.io/issues/5468
2026-04-17 09:55:51 +02:00
Abhijeet Abhi
4e85378a46 docs: fix various typos in test and documentation (#5481) 2026-04-17 07:48:08 +02:00
Damien Arrachequesne
d1f5aa9372 fix(eio): prevent WebTransport connections when a middleware is registered 2026-04-16 10:19:34 +02:00
Damien Arrachequesne
b7853771af docs: fix typos in the release notes
[skip ci]
2026-04-16 09:39:33 +02:00
Damien Arrachequesne
25d877cd9f docs: fix links in the release notes
For some reason, the links generated by `conventional-changelog` were wrong.
2026-04-16 09:23:56 +02:00
Damien Arrachequesne
d4bc787731 refactor(eio): use hasOwn() method everywhere 2026-04-16 09:22:10 +02:00
Damien Arrachequesne
1fa1f46cd4 fix(eio): handle invalid packets when upgrading to WebTransport 2026-04-16 09:08:40 +02:00
Damien Arrachequesne
4f7edb46ec ci: enable caching of npm modules
Note: `npm ci` is always required because it's the npm cache that is cached, not the node_modules/ directory.

Reference: https://github.com/actions/setup-node
2026-04-02 17:40:35 +02:00
Damien Arrachequesne
85b26e5c99 ci: re-enable tests with fetch and uws
Tests were skipped since [1].

[1]: b837949479
2026-04-01 10:20:57 +02:00
Damien Arrachequesne
e4d016bd5b docs(security): add CVE-2026-33151
[skip ci]
2026-03-18 09:09:12 +01:00
Varun Chawla
8b93a18681 fix(sio): allow emitWithAck() for events with void callbacks (#5453)
EventNamesWithAck previously excluded events whose callback had no
non-error arguments (e.g. `(cb: () => void) => void` or
`(cb: (err: Error) => void) => void`). This made it impossible to use
emitWithAck as a simple acknowledgement mechanism without data.

The fix removes the FirstNonErrorArg void check while keeping the guard
against events with no parameters at all, so events like `() => void`
(no callback) are still correctly excluded.

Related: https://github.com/socketio/socket.io/issues/5257
2026-03-17 18:56:40 +01:00
Damien Arrachequesne
e6c722edbe docs: add changelog for socket.io-parser 3.3.5 and 3.4.4
[skip ci]
2026-03-17 18:39:54 +01:00
Damien Arrachequesne
8b0ab0a9d9 chore(release): socket.io-parser@4.2.6
Diff: https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.5...socket.io-parser@4.2.6
2026-03-17 10:57:14 +01:00
Damien Arrachequesne
b25738c416 fix(parser): add a limit to the number of binary attachments
When a packet contains binary elements, the built-in parser does not modify them and simply sends them in their own WebSocket frame.

Example: `socket.emit("some event", Buffer.of(1,2,3))`

is encoded and transferred as:

- 1st frame: 51-["some event",{"_placeholder":true,"num":0}]
- 2nd frame: <buffer 01 02 03>

where:

- `5` is the type of the packet (binary message)
- `1` is the number of binary attachments
- `-` is the separator
- `["some event",{"_placeholder":true,"num":0}]` is the payload (including the placeholder)

On the receiving end, the parser reads the number of attachments and buffers them until they are all received.

Before this change, the built-in parser accepted any number of binary attachments, which could be exploited to make the server run out of memory.

The number of attachments is now limited to 10, which should be sufficient for most use cases.

The limit can be increased with a custom `parser`:

```js
import { Encoder, Decoder } from "socket.io-parser";

const io = new Server({
  parser: {
    Encoder,
    Decoder: class extends Decoder {
      constructor() {
        super({
          maxAttachments: 20
        });
      }
    }
  }
});
```
2026-03-17 10:57:13 +01:00
Sarthak Shah
f6301588ca fix(adapter): do not skip local broadcast when publishAndReturnOffset throws (#5457)
Remove the `return` in the catch block of ClusterAdapter.broadcast() so
that super.broadcast() is still called when remote publishing fails.

This ensures local sockets receive the event even if the cluster publish
errors out (e.g. due to a serialization error in the adapter layer).

Related: https://github.com/socketio/socket.io/issues/5456
2026-03-12 11:38:29 +01:00
22 changed files with 475 additions and 133 deletions

View File

@@ -37,6 +37,7 @@ jobs:
uses: actions/setup-node@v6
with:
node-version: 20
cache: npm
- name: Build ${{ matrix.example }}
run: |

View File

@@ -26,6 +26,7 @@ jobs:
uses: actions/setup-node@v6
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci

View File

@@ -55,6 +55,7 @@ jobs:
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: npm
- name: Install dependencies
run: npm ci
@@ -67,12 +68,12 @@ jobs:
- name: Run tests with uws (engine.io)
run: npm run test:uws --workspace=engine.io
if: ${{ matrix.node-version == '18' }}
if: ${{ matrix.node-version == '24' }}
- name: Run tests with fetch instead of XHR (engine.io-client)
run: npm run test:node-fetch --workspace=engine.io-client
if: ${{ matrix.node-version == '18' }}
if: ${{ matrix.node-version == '24' }}
- name: Run tests with Node.js native WebSocket (engine.io-client)
run: npm run test:node-builtin-ws --workspace=engine.io-client
if: ${{ matrix.node-version == '22' }}
if: ${{ matrix.node-version == '24' }}

View File

@@ -24,6 +24,7 @@ jobs:
with:
node-version: 24
registry-url: 'https://registry.npmjs.org'
cache: npm
- name: Install dependencies
run: npm ci

View File

@@ -37,33 +37,35 @@ We will get back to you as soon as possible and publish a fix if necessary.
From the transitive dependencies:
| Date | Dependency | Description | CVE number |
|---------------|--------------------|---------------------------------------------------------------------------------------------------------------|------------------|
| January 2016 | `ws` | [Buffer vulnerability](https://github.com/advisories/GHSA-2mhh-w6q8-5hxw) | `CVE-2016-10518` |
| January 2016 | `ws` | [DoS due to excessively large websocket message](https://github.com/advisories/GHSA-6663-c963-2gqg) | `CVE-2016-10542` |
| November 2017 | `ws` | [DoS in the `Sec-Websocket-Extensions` header parser](https://github.com/advisories/GHSA-5v72-xg48-5rpm) | `-` |
| February 2020 | `engine.io` | [Resource exhaustion](https://github.com/advisories/GHSA-j4f2-536g-r55m) | `CVE-2020-36048` |
| January 2021 | `socket.io-parser` | [Resource exhaustion](https://github.com/advisories/GHSA-xfhh-g9f5-x4m4) | `CVE-2020-36049` |
| May 2021 | `ws` | [ReDoS in `Sec-Websocket-Protocol` header](https://github.com/advisories/GHSA-6fc8-4gx4-v693) | `CVE-2021-32640` |
| January 2022 | `engine.io` | [Uncaught exception](https://github.com/advisories/GHSA-273r-mgr4-v34f) | `CVE-2022-21676` |
| October 2022 | `socket.io-parser` | [Insufficient validation when decoding a Socket.IO packet](https://github.com/advisories/GHSA-qm95-pgcg-qqfq) | `CVE-2022-2421` |
| November 2022 | `engine.io` | [Uncaught exception](https://github.com/advisories/GHSA-r7qp-cfhv-p84w) | `CVE-2022-41940` |
| May 2023 | `engine.io` | [Uncaught exception](https://github.com/advisories/GHSA-q9mw-68c2-j6m5) | `CVE-2023-31125` |
| May 2023 | `socket.io-parser` | [Insufficient validation when decoding a Socket.IO packet](https://github.com/advisories/GHSA-cqmj-92xf-r6r9) | `CVE-2023-32695` |
| June 2024 | `ws` | [DoS when handling a request with many HTTP headers](https://github.com/advisories/GHSA-3h5v-q93c-6h6q) | `CVE-2024-37890` |
| Date | Dependency | Description | CVE number |
|---------------|--------------------|-------------------------------------------------------------------------------------------------------------------------|------------------|
| January 2016 | `ws` | [Buffer vulnerability](https://github.com/advisories/GHSA-2mhh-w6q8-5hxw) | `CVE-2016-10518` |
| January 2016 | `ws` | [DoS due to excessively large websocket message](https://github.com/advisories/GHSA-6663-c963-2gqg) | `CVE-2016-10542` |
| November 2017 | `ws` | [DoS in the `Sec-Websocket-Extensions` header parser](https://github.com/advisories/GHSA-5v72-xg48-5rpm) | `-` |
| February 2020 | `engine.io` | [Resource exhaustion](https://github.com/advisories/GHSA-j4f2-536g-r55m) | `CVE-2020-36048` |
| January 2021 | `socket.io-parser` | [Resource exhaustion](https://github.com/advisories/GHSA-xfhh-g9f5-x4m4) | `CVE-2020-36049` |
| May 2021 | `ws` | [ReDoS in `Sec-Websocket-Protocol` header](https://github.com/advisories/GHSA-6fc8-4gx4-v693) | `CVE-2021-32640` |
| January 2022 | `engine.io` | [Uncaught exception](https://github.com/advisories/GHSA-273r-mgr4-v34f) | `CVE-2022-21676` |
| October 2022 | `socket.io-parser` | [Insufficient validation when decoding a Socket.IO packet](https://github.com/advisories/GHSA-qm95-pgcg-qqfq) | `CVE-2022-2421` |
| November 2022 | `engine.io` | [Uncaught exception](https://github.com/advisories/GHSA-r7qp-cfhv-p84w) | `CVE-2022-41940` |
| May 2023 | `engine.io` | [Uncaught exception](https://github.com/advisories/GHSA-q9mw-68c2-j6m5) | `CVE-2023-31125` |
| May 2023 | `socket.io-parser` | [Insufficient validation when decoding a Socket.IO packet](https://github.com/advisories/GHSA-cqmj-92xf-r6r9) | `CVE-2023-32695` |
| June 2024 | `ws` | [DoS when handling a request with many HTTP headers](https://github.com/advisories/GHSA-3h5v-q93c-6h6q) | `CVE-2024-37890` |
| March 2026 | `socket.io-parser` | [Unbounded number of binary attachments](https://github.com/socketio/socket.io/security/advisories/GHSA-677m-j7p3-52f9) | `CVE-2026-33151` |
### For the `socket.io-client` package
From the transitive dependencies:
| Date | Dependency | Description | CVE number |
|---------------|--------------------|---------------------------------------------------------------------------------------------------------------|------------------|
| January 2016 | `ws` | [Buffer vulnerability](https://github.com/advisories/GHSA-2mhh-w6q8-5hxw) | `CVE-2016-10518` |
| January 2016 | `ws` | [DoS due to excessively large websocket message](https://github.com/advisories/GHSA-6663-c963-2gqg) | `CVE-2016-10542` |
| October 2016 | `engine.io-client` | [Insecure Defaults Allow MITM Over TLS](https://github.com/advisories/GHSA-4r4m-hjwj-43p8) | `CVE-2016-10536` |
| November 2017 | `ws` | [DoS in the `Sec-Websocket-Extensions` header parser](https://github.com/advisories/GHSA-5v72-xg48-5rpm) | `-` |
| January 2021 | `socket.io-parser` | [Resource exhaustion](https://github.com/advisories/GHSA-xfhh-g9f5-x4m4) | `CVE-2020-36049` |
| May 2021 | `ws` | [ReDoS in `Sec-Websocket-Protocol` header](https://github.com/advisories/GHSA-6fc8-4gx4-v693) | `CVE-2021-32640` |
| October 2022 | `socket.io-parser` | [Insufficient validation when decoding a Socket.IO packet](https://github.com/advisories/GHSA-qm95-pgcg-qqfq) | `CVE-2022-2421` |
| May 2023 | `socket.io-parser` | [Insufficient validation when decoding a Socket.IO packet](https://github.com/advisories/GHSA-cqmj-92xf-r6r9) | `CVE-2023-32695` |
| June 2024 | `ws` | [DoS when handling a request with many HTTP headers](https://github.com/advisories/GHSA-3h5v-q93c-6h6q) | `CVE-2024-37890` |
| Date | Dependency | Description | CVE number |
|---------------|--------------------|-------------------------------------------------------------------------------------------------------------------------|------------------|
| January 2016 | `ws` | [Buffer vulnerability](https://github.com/advisories/GHSA-2mhh-w6q8-5hxw) | `CVE-2016-10518` |
| January 2016 | `ws` | [DoS due to excessively large websocket message](https://github.com/advisories/GHSA-6663-c963-2gqg) | `CVE-2016-10542` |
| October 2016 | `engine.io-client` | [Insecure Defaults Allow MITM Over TLS](https://github.com/advisories/GHSA-4r4m-hjwj-43p8) | `CVE-2016-10536` |
| November 2017 | `ws` | [DoS in the `Sec-Websocket-Extensions` header parser](https://github.com/advisories/GHSA-5v72-xg48-5rpm) | `-` |
| January 2021 | `socket.io-parser` | [Resource exhaustion](https://github.com/advisories/GHSA-xfhh-g9f5-x4m4) | `CVE-2020-36049` |
| May 2021 | `ws` | [ReDoS in `Sec-Websocket-Protocol` header](https://github.com/advisories/GHSA-6fc8-4gx4-v693) | `CVE-2021-32640` |
| October 2022 | `socket.io-parser` | [Insufficient validation when decoding a Socket.IO packet](https://github.com/advisories/GHSA-qm95-pgcg-qqfq) | `CVE-2022-2421` |
| May 2023 | `socket.io-parser` | [Insufficient validation when decoding a Socket.IO packet](https://github.com/advisories/GHSA-cqmj-92xf-r6r9) | `CVE-2023-32695` |
| June 2024 | `ws` | [DoS when handling a request with many HTTP headers](https://github.com/advisories/GHSA-3h5v-q93c-6h6q) | `CVE-2024-37890` |
| March 2026 | `socket.io-parser` | [Unbounded number of binary attachments](https://github.com/socketio/socket.io/security/advisories/GHSA-677m-j7p3-52f9) | `CVE-2026-33151` |

View File

@@ -296,7 +296,7 @@ A payload is a series of encoded packets tied together. The payload encoding for
<length1>:<packet1>[<length2>:<packet2>[...]]
```
* length: length of the packet in __characters__
* packet: actual packets as descriped above
* packet: actual packets as described above
When XHR2 is not supported, the same encoding principle is used also when
binary data is sent, but it is sent as base64 encoded strings. For the purposes of decoding, an identifier `b` is

View File

@@ -48,13 +48,30 @@
| [3.4.2](#342-2020-06-04) | June 2020 | `"` |
| [3.4.1](#341-2020-04-17) | April 2020 | `^7.1.2` |
## [6.6.7](https://github.com/socketio/socket.io/compare/engine.io@6.6.6...engine.io@6.6.7) (2026-04-27)
### Bug Fixes
* close HTTP requests with invalid content type ([fc11285](https://github.com/socketio/socket.io/commit/fc11285e14964c2132d122164bf130c355f60671))
* handle invalid packets when upgrading to WebTransport ([1fa1f46](https://github.com/socketio/socket.io/commit/1fa1f46cd420ac5b57bb4c04c959b58f3c79158c))
* prevent WebTransport connections when a middleware is registered ([d1f5aa9](https://github.com/socketio/socket.io/commit/d1f5aa93722a7f1ed729b96f771daf92a3dfdaf7))
### Dependencies
- [`ws@~8.18.3`](https://github.com/websockets/ws/releases/tag/8.18.3) (no change)
## [6.6.6](https://github.com/socketio/socket.io/compare/engine.io@6.6.5...engine.io@6.6.6) (2026-03-10)
### Bug Fixes
* add `@types/ws` as dependency ([#5458](https://github.com/socketio/socket/issues/5458)) ([07cbe15](https://github.com/socketio/socket/commit/07cbe1510ded7e5460cb82e026e2533e50e30eaf))
* **uws** emit initial_headers and headers events in uServer ([#5460](https://github.com/socketio/socket/issues/5460)) ([44ed73f](https://github.com/socketio/socket/commit/44ed73f53995d35ef0c8d10df6806d5687238282))
* add `@types/ws` as dependency ([#5458](https://github.com/socketio/socket.io/issues/5458)) ([07cbe15](https://github.com/socketio/socket.io/commit/07cbe1510ded7e5460cb82e026e2533e50e30eaf))
* **uws** emit initial_headers and headers events in uServer ([#5460](https://github.com/socketio/socket.io/issues/5460)) ([44ed73f](https://github.com/socketio/socket.io/commit/44ed73f53995d35ef0c8d10df6806d5687238282))
### Dependencies
@@ -138,7 +155,7 @@ See also: https://github.com/advisories/GHSA-pxg6-pf52-xh8x
### Performance Improvements
* do not reset the hearbeat timer on each packet ([5359bae](https://github.com/socketio/engine.io/commit/5359bae683e2a25742bd4989d0355a8fc10d294e))
* do not reset the heartbeat timer on each packet ([5359bae](https://github.com/socketio/engine.io/commit/5359bae683e2a25742bd4989d0355a8fc10d294e))
* **websocket:** use bound callbacks ([9a68c8c](https://github.com/socketio/engine.io/commit/9a68c8ce93cc1bc0bc1a30548558da49860f4acd))
@@ -535,9 +552,9 @@ Please upgrade as soon as possible.
* decrease the default value of maxHttpBufferSize ([58e274c](https://github.com/socketio/engine.io/commit/58e274c437e9cbcf69fd913c813aad8fbd253703))
This change reduces the default value from 100 mb to a more sane 1 mb.
This change reduces the default value from 100 MB to a saner 1 MB.
This helps protect the server against denial of service attacks by malicious clients sending huge amounts of data.
This helps protect the server against denial-of-service attacks by malicious clients sending huge amounts of data.
See also: https://github.com/advisories/GHSA-j4f2-536g-r55m
@@ -555,7 +572,7 @@ See also: https://github.com/advisories/GHSA-j4f2-536g-r55m
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
This is a backward compatible change that 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:
```
@@ -641,7 +658,7 @@ The codebase was migrated to TypeScript ([c0d6eaa](https://github.com/socketio/e
An ES module wrapper was also added ([401f4b6](https://github.com/socketio/engine.io/commit/401f4b60693fb6702c942692ce42e5bb701d81d7)).
Please note that the communication protocol was not updated, so a v5 client will be able to reach a v6 server (and vice-versa).
Please note that the communication protocol was not updated, so a v5 client will be able to reach a v6 server (and vice versa).
Reference: https://github.com/socketio/engine.io-protocol

View File

@@ -166,6 +166,11 @@ function parseSessionId(data: string): string | undefined {
} catch (e) {}
}
// Object.hasOwn() was introduced in Node.js 16.9
function hasOwn(obj: Record<string, any>, key: string): boolean {
return Object.prototype.hasOwnProperty.call(obj, key);
}
export abstract class BaseServer extends EventEmitter {
public opts: ServerOptions;
@@ -298,7 +303,7 @@ export abstract class BaseServer extends EventEmitter {
// sid check
const sid = req._query.sid;
if (sid) {
if (!this.clients.hasOwnProperty(sid)) {
if (!hasOwn(this.clients, sid)) {
debug('unknown sid "%s"', sid);
return fn(Server.errors.UNKNOWN_SID, {
sid,
@@ -398,9 +403,9 @@ export abstract class BaseServer extends EventEmitter {
*/
public close() {
debug("closing all open clients");
for (let i in this.clients) {
if (this.clients.hasOwnProperty(i)) {
this.clients[i].close(true);
for (const sid in this.clients) {
if (hasOwn(this.clients, sid)) {
this.clients[sid].close(true);
}
}
this.cleanup();
@@ -524,6 +529,15 @@ export abstract class BaseServer extends EventEmitter {
}
public async onWebTransportSession(session: any) {
if (this.middlewares.length > 0) {
// middlewares expect an IncomingMessage argument, which cannot be created from the WebTransport session object
// see also: https://github.com/fails-components/webtransport/issues/448
debug(
"closing session since WebTransport is not compatible with middlewares",
);
return session.close();
}
const timeout = setTimeout(() => {
debug(
"the client failed to establish a bidirectional stream in the given period",
@@ -583,7 +597,7 @@ export abstract class BaseServer extends EventEmitter {
const sid = parseSessionId(value.data);
if (!sid) {
if (!sid || !hasOwn(this.clients, sid)) {
debug("invalid WebTransport handshake");
return session.close();
}
@@ -750,18 +764,20 @@ export class Server extends BaseServer {
/**
* Handles an Engine.IO HTTP request.
*
* @param {EngineRequest} req
* @param {IncomingMessage} req
* @param {ServerResponse} res
*/
public handleRequest(req: EngineRequest, res: ServerResponse) {
public handleRequest(req: IncomingMessage, res: ServerResponse) {
debug('handling "%s" http request "%s"', req.method, req.url);
this.prepare(req);
req.res = res;
const engineRequest = req as EngineRequest;
this.prepare(engineRequest);
engineRequest.res = res;
const callback: ErrorCallback = (errorCode, errorContext) => {
if (errorCode !== undefined) {
this.emit("connection_error", {
req,
req: engineRequest,
code: errorCode,
message: Server.errorMessages[errorCode],
context: errorContext,
@@ -770,25 +786,27 @@ export class Server extends BaseServer {
return;
}
if (req._query.sid) {
if (engineRequest._query.sid) {
debug("setting new request for existing client");
this.clients[req._query.sid].transport.onRequest(req);
this.clients[engineRequest._query.sid].transport.onRequest(
engineRequest,
);
} else {
const closeConnection = (errorCode, errorContext) =>
abortRequest(res, errorCode, errorContext);
this.handshake(
req._query.transport as TransportName,
req,
engineRequest._query.transport as TransportName,
engineRequest,
closeConnection,
);
}
};
this._applyMiddlewares(req, res, (err) => {
this._applyMiddlewares(engineRequest, res, (err) => {
if (err) {
callback(Server.errors.BAD_REQUEST, { name: "MIDDLEWARE_FAILURE" });
} else {
this.verify(req, false, callback);
this.verify(engineRequest, false, callback);
}
});
}
@@ -797,17 +815,19 @@ export class Server extends BaseServer {
* Handles an Engine.IO HTTP Upgrade.
*/
public handleUpgrade(
req: EngineRequest,
req: IncomingMessage,
socket: Duplex,
upgradeHead: Buffer,
) {
this.prepare(req);
const engineRequest = req as EngineRequest;
const res = new WebSocketResponse(req, socket);
this.prepare(engineRequest);
const res = new WebSocketResponse(engineRequest, socket);
const callback: ErrorCallback = (errorCode, errorContext) => {
if (errorCode !== undefined) {
this.emit("connection_error", {
req,
req: engineRequest,
code: errorCode,
message: Server.errorMessages[errorCode],
context: errorContext,
@@ -824,18 +844,22 @@ export class Server extends BaseServer {
res.writeHead();
// delegate to ws
this.ws.handleUpgrade(req, socket, head, (websocket) => {
this.onWebSocket(req, socket, websocket);
this.ws.handleUpgrade(engineRequest, socket, head, (websocket) => {
this.onWebSocket(engineRequest, socket, websocket);
});
};
this._applyMiddlewares(req, res as unknown as ServerResponse, (err) => {
if (err) {
callback(Server.errors.BAD_REQUEST, { name: "MIDDLEWARE_FAILURE" });
} else {
this.verify(req, true, callback);
}
});
this._applyMiddlewares(
engineRequest,
res as unknown as ServerResponse,
(err) => {
if (err) {
callback(Server.errors.BAD_REQUEST, { name: "MIDDLEWARE_FAILURE" });
} else {
this.verify(engineRequest, true, callback);
}
},
);
}
/**
@@ -933,7 +957,7 @@ export class Server extends BaseServer {
server.on("request", (req, res) => {
if (check(req)) {
debug('intercepting request for path "%s"', path);
this.handleRequest(req as EngineRequest, res);
this.handleRequest(req, res);
} else {
let i = 0;
const l = listeners.length;
@@ -946,7 +970,7 @@ export class Server extends BaseServer {
if (~this.opts.transports.indexOf("websocket")) {
server.on("upgrade", (req, socket, head) => {
if (check(req)) {
this.handleUpgrade(req as EngineRequest, socket, head);
this.handleUpgrade(req, socket, head);
} else if (false !== options.destroyUpgrade) {
// default node behavior is to disconnect when no handlers
// but by adding a handler, we prevent that

View File

@@ -136,7 +136,8 @@ export class Polling extends Transport {
const isBinary = "application/octet-stream" === req.headers["content-type"];
if (isBinary && this.protocol === 4) {
return this.onError("invalid content");
this.onError("invalid content");
return res.writeStatus("400 Bad Request").end();
}
this.dataReq = req;

View File

@@ -122,7 +122,8 @@ export class Polling extends Transport {
const isBinary = "application/octet-stream" === req.headers["content-type"];
if (isBinary && this.protocol === 4) {
return this.onError("invalid content");
this.onError("invalid content");
return res.writeHead(400).end();
}
this.dataReq = req;

View File

@@ -1,6 +1,6 @@
{
"name": "engine.io",
"version": "6.6.6",
"version": "6.6.7",
"description": "The realtime engine behind Socket.IO. Provides the foundation of a bidirectional connection between client and server",
"type": "commonjs",
"main": "./build/engine.io.js",

View File

@@ -60,6 +60,32 @@ exports.listen = (opts, fn) => {
return e;
};
exports.listenAsync = function listenAsync(opts = {}) {
return new Promise((resolve) => {
const engine = exports.listen(opts, (port) => {
resolve({
port,
close: () => {
engine.close();
if (engine.httpServer) {
engine.httpServer.close();
}
},
});
});
});
};
exports.runHandshake = async function runHandshake(port) {
const res = await fetch(
`http://localhost:${port}/engine.io/?EIO=4&transport=polling`,
);
const data = await res.text();
return {
sid: JSON.parse(data.substring(1)).sid,
};
};
exports.ClientSocket = Socket;
exports.createPartialDone = (done, count) => {

View File

@@ -7,7 +7,13 @@ const path = require("path");
const exec = require("child_process").exec;
const zlib = require("zlib");
const { Server, Socket, attach } = require("..");
const { ClientSocket, listen, createPartialDone } = require("./common");
const {
ClientSocket,
listen,
listenAsync,
runHandshake,
createPartialDone,
} = require("./common");
const expect = require("expect.js");
const request = require("superagent");
const cookieMod = require("cookie");
@@ -581,7 +587,7 @@ describe("server", () => {
});
});
it("should not suggest upgrades when none are availble", (done) => {
it("should not suggest upgrades when none are available", (done) => {
listen({ transports: ["polling"] }, (port) => {
const socket = new ClientSocket(`ws://localhost:${port}`, {});
socket.on("handshake", (obj) => {
@@ -1458,6 +1464,26 @@ describe("server", () => {
},
);
it("should abort the polling data request if the content type is invalid", async () => {
const { port, close } = await listenAsync();
const { sid } = await runHandshake(port);
const res = await fetch(
`http://localhost:${port}/engine.io/?EIO=4&transport=polling&sid=${sid}`,
{
method: "POST",
headers: {
"content-type": "application/octet-stream",
},
body: Buffer.of(1, 2, 3),
},
);
expect(res.status).to.eql(400);
close();
});
// tests https://github.com/LearnBoost/engine.io-client/issues/207
// websocket test, transport error
it("should trigger transport close before open for ws", (done) => {

View File

@@ -302,6 +302,122 @@ describe("WebTransport", () => {
);
});
it("should close a connection that sends an invalid upgrade", (done) => {
setupServer(
{
transports: ["polling", "websocket", "webtransport"],
},
async ({ engine, h3Server, certificate }) => {
const httpServer = await createHttpServer(h3Server.port);
engine.attach(httpServer);
request(`http://localhost:${h3Server.port}/engine.io/`)
.query({ EIO: 4, transport: "polling" })
.end(async (_, res) => {
const payload = JSON.parse(res.text.substring(1));
expect(payload.upgrades).to.eql(["websocket", "webtransport"]);
const client = new WebTransport(
`https://127.0.0.1:${h3Server.port}/engine.io/`,
{
serverCertificateHashes: [
{
algorithm: "sha-256",
value: certificate.hash,
},
],
},
);
await client.ready;
const stream = await client.createBidirectionalStream();
const writer = stream.writable.getWriter();
await writer.write(Uint8Array.of(31));
await writer.write(
TEXT_ENCODER.encode(`0{"sid":"11111111111111111111"}`),
);
client.closed.then(() => {
success(engine, h3Server, done);
});
});
},
);
});
it("should close a connection that sends an invalid upgrade (bis)", (done) => {
setupServer(
{
transports: ["polling", "websocket", "webtransport"],
},
async ({ engine, h3Server, certificate }) => {
const httpServer = await createHttpServer(h3Server.port);
engine.attach(httpServer);
request(`http://localhost:${h3Server.port}/engine.io/`)
.query({ EIO: 4, transport: "polling" })
.end(async (_, res) => {
const payload = JSON.parse(res.text.substring(1));
expect(payload.upgrades).to.eql(["websocket", "webtransport"]);
const client = new WebTransport(
`https://127.0.0.1:${h3Server.port}/engine.io/`,
{
serverCertificateHashes: [
{
algorithm: "sha-256",
value: certificate.hash,
},
],
},
);
await client.ready;
const stream = await client.createBidirectionalStream();
const writer = stream.writable.getWriter();
await writer.write(Uint8Array.of(20));
await writer.write(TEXT_ENCODER.encode(`0{"sid":"__proto__"}`));
client.closed.then(() => {
success(engine, h3Server, done);
});
});
},
);
});
it("should refuse the connection when a middleware is registered", (done) => {
setupServer({}, async ({ engine, h3Server, certificate }) => {
engine.use((req, res, next) => next());
engine.on("connection", () => {
done(new Error("should not happen"));
});
const client = new WebTransport(
`https://127.0.0.1:${h3Server.port}/engine.io/`,
{
serverCertificateHashes: [
{
algorithm: "sha-256",
value: certificate.hash,
},
],
},
);
await client.closed;
success(engine, h3Server, done);
});
});
it("should send ping/pong packets", (done) => {
setup(
{

View File

@@ -438,11 +438,7 @@ export abstract class ClusterAdapter extends Adapter {
});
this.addOffsetIfNecessary(packet, opts, offset);
} catch (e) {
return debug(
"[%s] error while broadcasting message: %s",
this.uid,
e.message,
);
debug("[%s] error while broadcasting message: %s", this.uid, e.message);
}
}

View File

@@ -15,6 +15,7 @@ const NODES_COUNT = 3;
class EventEmitterAdapter extends ClusterAdapterWithHeartbeat {
private offset = 1;
public shouldFailPublish = false;
constructor(
nsp,
@@ -27,6 +28,9 @@ class EventEmitterAdapter extends ClusterAdapterWithHeartbeat {
}
protected doPublish(message: ClusterMessage): Promise<string> {
if (this.shouldFailPublish) {
return Promise.reject(new Error("publish failed"));
}
this.eventBus.emit("message", message);
return Promise.resolve(String(++this.offset));
}
@@ -152,6 +156,19 @@ describe("cluster adapter", () => {
servers[0].local.emit("test");
});
it("broadcasts to local clients even when publishAndReturnOffset throws", (done) => {
const adapter = servers[0].of("/").adapter as EventEmitterAdapter;
adapter.shouldFailPublish = true;
clientSockets[0].on("test", (arg1) => {
expect(arg1).to.eql(1);
adapter.shouldFailPublish = false;
done();
});
servers[0].emit("test", 1);
});
it("broadcasts with multiple acknowledgements", (done) => {
clientSockets[0].on("test", (cb) => cb(1));
clientSockets[1].on("test", (cb) => cb(2));

View File

@@ -1,37 +1,66 @@
# Changelog
| Version | Release date |
|-------------------------------------------------------------------------------------------------------------|----------------|
| [4.2.5](#425-2025-12-23) | December 2025 |
| [3.3.4](#334-2024-07-22) (from the [3.3.x](https://github.com/socketio/socket.io-parser/tree/3.3.x) branch) | July 2024 |
| [4.2.4](#424-2023-05-31) | May 2023 |
| [3.4.3](#343-2023-05-22) (from the [3.4.x](https://github.com/socketio/socket.io-parser/tree/3.4.x) branch) | May 2023 |
| [4.2.3](#423-2023-05-22) | May 2023 |
| [4.2.2](#422-2023-01-19) | January 2023 |
| [3.3.3](#333-2022-11-09) (from the [3.3.x](https://github.com/socketio/socket.io-parser/tree/3.3.x) branch) | November 2022 |
| [3.4.2](#342-2022-11-09) (from the [3.4.x](https://github.com/socketio/socket.io-parser/tree/3.4.x) branch) | November 2022 |
| [4.0.5](#405-2022-06-27) (from the [4.0.x](https://github.com/socketio/socket.io-parser/tree/4.0.x) branch) | June 2022 |
| [4.2.1](#421-2022-06-27) | June 2022 |
| [4.2.0](#420-2022-04-17) | April 2022 |
| [4.1.2](#412-2022-02-17) | February 2022 |
| [3.3.3](#333-2022-11-09) (from the [3.3.x](https://github.com/socketio/socket.io-parser/tree/3.3.x) branch) | November 2022 |
| [3.4.2](#342-2022-11-09) (from the [3.4.x](https://github.com/socketio/socket.io-parser/tree/3.4.x) branch) | November 2022 |
| [4.0.5](#405-2022-06-27) (from the [4.0.x](https://github.com/socketio/socket.io-parser/tree/4.0.x) branch) | June 2022 |
| [4.2.1](#421-2022-06-27) | June 2022 |
| [4.2.0](#420-2022-04-17) | April 2022 |
| [4.1.2](#412-2022-02-17) | February 2022 |
| [4.1.1](#411-2021-10-14) | October 2021 |
| [4.1.0](#410-2021-10-11) | October 2021 |
| [4.0.4](#404-2021-01-15) | January 2021 |
| [3.3.2](#332-2021-01-09) (from the [3.3.x](https://github.com/socketio/socket.io-parser/tree/3.3.x) branch) | January 2021 |
| [4.0.3](#403-2021-01-05) | January 2021 |
| [4.0.2](#402-2020-11-25) | November 2020 |
| [4.0.1](#401-2020-11-05) | November 2020 |
| [3.3.1](#331-2020-09-30) (from the [3.3.x](https://github.com/socketio/socket.io-parser/tree/3.3.x) branch) | September 2020 |
| [**4.0.0**](#400-2020-09-28) | September 2020 |
| [3.4.1](#341-2020-05-13) | May 2020 |
| [3.4.0](#340-2019-09-20) | September 2019 |
| [3.3.0](#330-2018-11-07) | November 2018 |
| Version | Release date |
|-----------------------------------------------------------------------------------------------------------------------|----------------|
| [3.4.4](#344-2026-03-17) (from the [3.4.x](https://github.com/socketio/socket.io/tree/socket.io-parser/3.4.x) branch) | March 2026 |
| [3.3.5](#335-2026-03-17) (from the [3.3.x](https://github.com/socketio/socket.io/tree/socket.io-parser/3.3.x) branch) | March 2026 |
| [4.2.6](#426-2026-03-17) | March 2026 |
| [4.2.5](#425-2025-12-23) | December 2025 |
| [3.3.4](#334-2024-07-22) (from the [3.3.x](https://github.com/socketio/socket.io-parser/tree/3.3.x) branch) | July 2024 |
| [4.2.4](#424-2023-05-31) | May 2023 |
| [3.4.3](#343-2023-05-22) (from the [3.4.x](https://github.com/socketio/socket.io-parser/tree/3.4.x) branch) | May 2023 |
| [4.2.3](#423-2023-05-22) | May 2023 |
| [4.2.2](#422-2023-01-19) | January 2023 |
| [3.3.3](#333-2022-11-09) (from the [3.3.x](https://github.com/socketio/socket.io-parser/tree/3.3.x) branch) | November 2022 |
| [3.4.2](#342-2022-11-09) (from the [3.4.x](https://github.com/socketio/socket.io-parser/tree/3.4.x) branch) | November 2022 |
| [4.0.5](#405-2022-06-27) (from the [4.0.x](https://github.com/socketio/socket.io-parser/tree/4.0.x) branch) | June 2022 |
| [4.2.1](#421-2022-06-27) | June 2022 |
| [4.2.0](#420-2022-04-17) | April 2022 |
| [4.1.2](#412-2022-02-17) | February 2022 |
| [3.3.3](#333-2022-11-09) (from the [3.3.x](https://github.com/socketio/socket.io-parser/tree/3.3.x) branch) | November 2022 |
| [3.4.2](#342-2022-11-09) (from the [3.4.x](https://github.com/socketio/socket.io-parser/tree/3.4.x) branch) | November 2022 |
| [4.0.5](#405-2022-06-27) (from the [4.0.x](https://github.com/socketio/socket.io-parser/tree/4.0.x) branch) | June 2022 |
| [4.2.1](#421-2022-06-27) | June 2022 |
| [4.2.0](#420-2022-04-17) | April 2022 |
| [4.1.2](#412-2022-02-17) | February 2022 |
| [4.1.1](#411-2021-10-14) | October 2021 |
| [4.1.0](#410-2021-10-11) | October 2021 |
| [4.0.4](#404-2021-01-15) | January 2021 |
| [3.3.2](#332-2021-01-09) (from the [3.3.x](https://github.com/socketio/socket.io-parser/tree/3.3.x) branch) | January 2021 |
| [4.0.3](#403-2021-01-05) | January 2021 |
| [4.0.2](#402-2020-11-25) | November 2020 |
| [4.0.1](#401-2020-11-05) | November 2020 |
| [3.3.1](#331-2020-09-30) (from the [3.3.x](https://github.com/socketio/socket.io-parser/tree/3.3.x) branch) | September 2020 |
| [**4.0.0**](#400-2020-09-28) | September 2020 |
| [3.4.1](#341-2020-05-13) | May 2020 |
| [3.4.0](#340-2019-09-20) | September 2019 |
| [3.3.0](#330-2018-11-07) | November 2018 |
## [3.4.4](https://github.com/socketio/socket.io-parser/compare/3.4.3...3.4.4) (2026-03-17)
### Bug Fixes
* add a limit to the number of binary attachments ([719f9eb](https://github.com/socketio/socket.io/commit/719f9ebab0772ffb882bd614b387e585c1aa75d4))
## [3.3.5](https://github.com/socketio/socket.io-parser/compare/3.3.4...3.3.5) (2026-03-17)
### Bug Fixes
* add a limit to the number of binary attachments ([9d39f1f](https://github.com/socketio/socket.io/commit/9d39f1f080510f036782f2177fac701cc041faaf))
## [4.2.6](https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.5...socket.io-parser@4.2.6) (2026-03-17)
### Bug Fixes
* **parser:** add a limit to the number of binary attachments ([3fff7ca](https://github.com/socketio/socket.io/commit/3fff7cafa98f1ba5840475b6917c651fe841a943))
## [4.2.5](https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.4...socket.io-parser@4.2.5) (2025-12-23)

View File

@@ -135,6 +135,20 @@ interface DecoderReservedEvents {
decoded: (packet: Packet) => void;
}
type JSONReviver = (this: any, key: string, value: any) => any;
export interface DecoderOptions {
/**
* Custom reviver to pass down to JSON.parse()
*/
reviver?: JSONReviver;
/**
* Maximum number of binary attachments per packet
* @default 10
*/
maxAttachments?: number;
}
/**
* A socket.io Decoder instance
*
@@ -142,14 +156,20 @@ interface DecoderReservedEvents {
*/
export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
private reconstructor: BinaryReconstructor;
private opts: Required<DecoderOptions>;
/**
* Decoder constructor
*
* @param {function} reviver - custom reviver to pass down to JSON.stringify
*/
constructor(private reviver?: (this: any, key: string, value: any) => any) {
constructor(opts?: DecoderOptions | JSONReviver) {
super();
this.opts = Object.assign(
{
reviver: undefined,
maxAttachments: 10,
},
typeof opts === "function" ? { reviver: opts } : opts,
);
}
/**
@@ -224,7 +244,13 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
if (buf != Number(buf) || str.charAt(i) !== "-") {
throw new Error("Illegal attachments");
}
p.attachments = Number(buf);
const n = Number(buf);
if (!isInteger(n) || n < 0) {
throw new Error("Illegal attachments");
} else if (n > this.opts.maxAttachments) {
throw new Error("too many attachments");
}
p.attachments = n;
}
// look up namespace (if any)
@@ -271,7 +297,7 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
private tryParse(str) {
try {
return JSON.parse(str, this.reviver);
return JSON.parse(str, this.opts.reviver);
} catch (e) {
return false;
}

View File

@@ -1,6 +1,6 @@
{
"name": "socket.io-parser",
"version": "4.2.5",
"version": "4.2.6",
"description": "socket.io protocol parser",
"homepage": "https://github.com/socketio/socket.io/tree/main/packages/socket.io-client#readme",
"repository": {

View File

@@ -107,6 +107,56 @@ describe("socket.io-parser", () => {
}
});
it("throws an error when receiving too many attachments", () => {
const decoder = new Decoder({ maxAttachments: 2 });
expect(() => {
decoder.add(
'53-["hello",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1},{"_placeholder":true,"num":2}]',
);
}).to.throwException(/^too many attachments$/);
});
it("decodes with a custom reviver", () => {
const decoder = new Decoder((key, value) => {
if (key === "a") {
return value.toUpperCase();
} else {
return value;
}
});
return new Promise((resolve) => {
decoder.on("decoded", (packet) => {
expect(packet.data).to.eql(["b", { a: "VAL" }]);
resolve();
});
decoder.add('2["b",{"a":"val"}]');
});
});
it("decodes with a custom reviver (options object)", () => {
const decoder = new Decoder({
reviver: (key, value) => {
if (key === "a") {
return value.toUpperCase();
} else {
return value;
}
},
});
return new Promise((resolve) => {
decoder.on("decoded", (packet) => {
expect(packet.data).to.eql(["b", { a: "VAL" }]);
resolve();
});
decoder.add('2["b",{"a":"val"}]');
});
});
it("throw an error upon parsing error", () => {
const isInvalidPayload = (str) =>
expect(() => new Decoder().add(str)).to.throwException(
@@ -125,6 +175,16 @@ describe("socket.io-parser", () => {
isInvalidPayload('2["connect"]');
isInvalidPayload('2["disconnect","123"]');
const isInvalidAttachmentCount = (str) =>
expect(() => new Decoder().add(str)).to.throwException(
/^Illegal attachments$/,
);
isInvalidAttachmentCount("5");
isInvalidAttachmentCount("51");
isInvalidAttachmentCount("5a-");
isInvalidAttachmentCount("51.23-");
expect(() => new Decoder().add("999")).to.throwException(
/^unknown packet type 9$/,
);

View File

@@ -22,8 +22,6 @@ export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol);
/**
* Returns a union type containing all the keys of an event map that have an acknowledgement callback.
*
* That also have *some* data coming in.
*/
export type EventNamesWithAck<
Map extends EventsMap,
@@ -32,11 +30,11 @@ export type EventNamesWithAck<
Last<Parameters<Map[K]>> | Map[K],
K,
K extends (
Last<Parameters<Map[K]>> extends (...args: any[]) => any
? FirstNonErrorArg<Last<Parameters<Map[K]>>> extends void
? never
: K
: never
Parameters<Map[K]> extends never[]
? never
: Last<Parameters<Map[K]>> extends (...args: any[]) => any
? K
: never
)
? K
: never

View File

@@ -265,15 +265,11 @@ describe("server", () => {
interface ServerToClientEventsWithMultipleWithAck {
ackFromServer: (a: boolean, b: string) => Promise<boolean[]>;
ackFromServerSingleArg: (a: boolean, b: string) => Promise<string[]>;
// This should technically be `undefined[]`, but this doesn't work currently *only* with emitWithAck
// you can use an empty callback with emit, but not emitWithAck
onlyCallback: () => Promise<undefined>;
}
interface ServerToClientEventsWithAck {
ackFromServer: (a: boolean, b: string) => Promise<boolean>;
ackFromServerSingleArg: (a: boolean, b: string) => Promise<string>;
// This doesn't work currently *only* with emitWithAck
// you can use an empty callback with emit, but not emitWithAck
onlyCallback: () => Promise<undefined>;
}
describe("Emitting Types", () => {
@@ -420,8 +416,9 @@ describe("server", () => {
sio.timeout(0).emitWithAck("noArgs");
// @ts-expect-error - "helloFromServer" doesn't have a callback and is thus excluded
sio.timeout(0).emitWithAck("helloFromServer");
// @ts-expect-error - "onlyCallback" doesn't have a callback and is thus excluded
sio.timeout(0).emitWithAck("onlyCallback");
expectType<
ToEmitWithAck<ServerToClientEventsWithMultipleWithAck, "onlyCallback">
>(sio.timeout(0).emitWithAck<"onlyCallback">);
expectType<
ToEmitWithAck<
ServerToClientEventsWithMultipleWithAck,
@@ -447,7 +444,7 @@ describe("server", () => {
nio.emit<"noArgs">,
);
expectType<ToEmit<ServerToClientEventsNoAck, "helloFromServer">>(
// These errors will dissapear once the TS version is updated from 4.7.4
// These errors will disappear once the TS version is updated from 4.7.4
// the TSD instance is using a newer version of TS than the workspace version
// to enable the ability to compare against `any`
sio.emit<"helloFromServer">,
@@ -496,10 +493,12 @@ describe("server", () => {
s.emitWithAck("noArgs");
// @ts-expect-error - "helloFromServer" doesn't have a callback and is thus excluded
s.emitWithAck("helloFromServer");
// @ts-expect-error - "onlyCallback" doesn't have a callback and is thus excluded
s.emitWithAck("onlyCallback");
// @ts-expect-error - "onlyCallback" doesn't have a callback and is thus excluded
s.timeout(0).emitWithAck("onlyCallback");
expectType<
ToEmitWithAck<ServerToClientEventsWithAck, "onlyCallback">
>(s.emitWithAck<"onlyCallback">);
expectType<
ToEmitWithAck<ServerToClientEventsWithAck, "onlyCallback">
>(s.timeout(0).emitWithAck<"onlyCallback">);
expectType<
ToEmitWithAck<ServerToClientEventsWithAck, "ackFromServerSingleArg">
>(s.emitWithAck<"ackFromServerSingleArg">);