Repository navigation
Description
Describe the bug
When the polling transport is closed, doClose() only sends a close packet (one more POST request). It does not abort the requests that are still pending, like the long-polling GET request (BaseXHR keeps it in this.pollXhr, but does not use it).
With an Engine.IO server, this is not visible: when the server receives the close packet, it answers the pending GET request. But when the GET request does not get an answer (a proxy that holds it, or a request on a connection that is no longer usable, for example after a network change), the request stays open after the close event. It holds a connection (in a browser, one of the few connections per origin), and in Node.js the process does not exit.
To Reproduce
Engine.IO client version: 6.6.8 (also present on main)
The server is a small HTTP server that answers the handshake and the POST requests, but never answers the long-polling GET request. The client closes the socket 100 ms after it is open, while the long-polling request is pending. The repro uses the Fetch transport (see the note below about xmlhttprequest-ssl).
package.json:
{
"private": true,
"type": "module",
"dependencies": {
"engine.io-client": "6.6.8"
},
"devDependencies": {
"@types/node": "24.19.2",
"typescript": "7.0.2"
}
}repro.ts:
// npm install, then: node repro.ts (Node.js 22.18 or later runs TypeScript directly)
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import { Fetch, Socket } from "engine.io-client";
let pending = false;
// A server (or a proxy) that answers the POST requests, but never answers the long-polling GET request.
const httpServer = createServer((req, res) => {
if (req.method === "POST") {
req.resume();
req.on("end", () => res.end("ok"));
return;
}
if (!new URL(req.url ?? "/", "http://x").searchParams.has("sid")) {
res.end('0{"sid":"abc","upgrades":[],"pingInterval":10000,"pingTimeout":5000,"maxPayload":1000000}');
return;
}
pending = true;
console.log("server: long-polling request received (never answered)");
res.on("close", () => {
pending = false;
console.log("server: long-polling request closed");
});
});
httpServer.listen(0, "127.0.0.1", () => {
const { port } = httpServer.address() as AddressInfo;
const socket = new Socket(`http://127.0.0.1:${port}`, { transports: [Fetch] });
// Close the socket while the long-polling request is pending.
socket.on("open", () => setTimeout(() => socket.close(), 100));
socket.on("close", (reason) => console.log(`client: close "${reason}"`));
setTimeout(() => {
console.log(`after 2 s, the long-polling request is still open: ${pending}`);
process.exit(pending ? 1 : 0);
}, 2000);
});npm install, then node repro.ts (Node.js 22.18 or later runs TypeScript directly). Output:
server: long-polling request received (never answered)
client: close "forced close"
after 2 s, the long-polling request is still open: true
The server never sees the long-polling request closed: it stays open after the close event, until process.exit().
Expected behavior
Once the close packet is sent, the pending requests are aborted, and the server prints server: long-polling request closed.
Platform:
- Device: a GitHub Actions runner
- OS: Linux, Node.js 24.22.2
Additional context
https://github.com/socketio/socket.io/blob/engine.io-client%406.6.8/packages/engine.io-client/lib/transports/polling.ts#L122-L137
https://github.com/socketio/socket.io/blob/engine.io-client%406.6.8/packages/engine.io-client/lib/transports/polling-xhr.ts#L82
The requests must not be aborted before the server receives the close packet: if the server sees the GET request closed first, it closes the session with the reason transport error instead of transport close.
Note: with the default polling transport of Node.js (xmlhttprequest-ssl), a pending request cannot be aborted today: from version 2.1.2 (the version that ~2.1.1 installs), abort() on a pending request throws an uncaught TypeError: Cannot read properties of null (reading 'reusedSocket') (mjwwit/node-XMLHttpRequest#37).
A pull request with a fix and a test follows: the pending requests are aborted once the close packet is sent (or cannot be sent), with the XMLHttpRequest object of a browser and with the Fetch transport.
I found this with AI assistance (Claude) during a review of WebSocket client libraries. The repro above was run with engine.io-client 6.6.8 on Node.js 24.22.2 on Linux (a GitHub Actions runner), and the output above is from that run.
Activity