Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

connection: Fix race condition when socket closes after timeout #1048

Merged
merged 1 commit into from
Jan 8, 2025
Merged
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
30 changes: 25 additions & 5 deletions packages/connection/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,18 @@ class Connection extends EventEmitter {
this.emit("error", error);
}

#onSocketClosed(dirty, event) {
this._reset();
this._status("disconnect", { clean: !dirty, event });
}

_attachSocket(socket) {
this.socket = socket;
const listeners = this.socketListeners;

listeners.data = this._onData.bind(this);

listeners.close = (dirty, event) => {
this._reset();
this._status("disconnect", { clean: !dirty, event });
};
listeners.close = this.#onSocketClosed.bind(this);

listeners.connect = () => {
this._status("connect");
Expand Down Expand Up @@ -178,6 +180,22 @@ class Connection extends EventEmitter {
return this.jid;
}

/*
[
"offline",
// "disconnect",
"connecting",
"connected",
"opening",
"open",
"online",
"closing",
"close",
"disconnecting",
"disconnect",
"offline",
];
*/
_status(status, ...args) {
if (this.status === status) return;
this.status = status;
Expand All @@ -201,7 +219,9 @@ class Connection extends EventEmitter {

try {
await this.disconnect();
} catch {}
} catch (err) {
this.#onSocketClosed(true, err);
}
Comment on lines -204 to +224
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual fix #onSocketClosed will call _detachSocket which will prevent the disconnect status from being fired if the socket close after a timeout error.


return el;
}
Expand Down
30 changes: 30 additions & 0 deletions packages/connection/test/stop.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Connection from "../index.js";
import { EventEmitter } from "@xmpp/events";

test("resolves if socket property is undefined", async () => {
const conn = new Connection();
Expand Down Expand Up @@ -38,3 +39,32 @@ test("does not throw if connection is not established", async () => {
await conn.stop();
expect().pass();
});

// https://github.com/xmppjs/xmpp.js/issues/956
test("socket closes after timeout", (done) => {
const conn = new Connection();
conn.timeout = 100;

const socket = new EventEmitter();
socket.end = jest.fn(async () => {
// Mock receiving "close" event after timeout
setTimeout(() => {
socket.emit("close");
}, conn.timeout * 2);
});
conn._attachSocket(socket);

const statuses = [conn.status];
conn.on("status", (status) => {
statuses.push(status);
});

conn.stop();

// Wait a bit and assert that status is correct
setTimeout(() => {
expect(conn.status).toBe("offline");
expect(conn.status).not.toBe("disconnect");
done();
}, conn.timeout * 3);
});
40 changes: 40 additions & 0 deletions test/client.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// eslint-disable-next-line n/no-extraneous-import
import { promise } from "@xmpp/events";
import { client, xml, jid } from "../packages/client/index.js";
import debug from "../packages/debug/index.js";
import server from "../server/index.js";
Expand Down Expand Up @@ -143,6 +145,44 @@ test("does not reconnect when stop is called", (done) => {
xmpp.start();
});

test("statuses", async () => {
xmpp = client({ credentials, service: domain });
debug(xmpp);

let statuses = [xmpp.status];

xmpp.on("status", (status) => {
statuses.push(status);
});

xmpp.on("error", () => {});

await xmpp.start();

expect(statuses).toEqual([
"offline",
"connecting",
"connect",
"opening",
"open",
"online",
]);

// trigger reconnect
await xmpp.disconnect();

statuses = [xmpp.status];
await promise(xmpp, "open");

expect(statuses).toEqual([
"disconnect",
"connecting",
"connect",
"opening",
"open",
]);
});

test("anonymous authentication", (done) => {
expect.assertions(2);

Expand Down
Loading