Skip to content

Handle reconnection for godot-socketio#7

Open
garciaf wants to merge 1 commit intomsabaeian:mainfrom
garciaf:feature/improvment
Open

Handle reconnection for godot-socketio#7
garciaf wants to merge 1 commit intomsabaeian:mainfrom
garciaf:feature/improvment

Conversation

@garciaf
Copy link
Copy Markdown

@garciaf garciaf commented Apr 5, 2026

Bug fixes

_on_noop() treated NOOP as an error

NOOP is a normal Engine.IO packet sent during polling to unblock a long-poll request. The handler was calling push_error(). It now calls _poll(), matching the behaviour of _on_message().

disconnect_socket() emitted socket_disconnected twice

disconnect_socket() called engine_close() which already emits engine_connection_closed_on_engine_io_connection_closed()socket_disconnected. The duplicate direct socket_disconnected.emit() at the end of disconnect_socket() was removed.

Upgrade PONG and heartbeat PONG not distinguished

_on_pong() was unconditionally triggering the WebSocket upgrade flow. It now receives the raw payload and only upgrades when the payload is "probe", correctly ignoring plain heartbeat PONGs from the server.

State and TransportType enum type annotation error

In Godot 4, inner enums on a class_name class cause a type mismatch between the unqualified (State) and fully qualified (EngineIO.State) names when used as type annotations. Removed the explicit type annotations on state and _transport_type, letting GDScript infer them from their initialisers.

Client-initiated PINGs broke Engine.IO v4

In Engine.IO v4 only the server sends PINGs — client-initiated PINGs are v3 behaviour and cause the server to disconnect with transport error. A heartbeat timer that was sending unsolicited PINGs on pingInterval has been removed. The existing _on_ping() → PONG response path is sufficient for keepalive.


New feature: automatic reconnection

When the WebSocket closes unexpectedly, the client now attempts to reconnect automatically with linear backoff instead of immediately calling engine_close().

Three new exported variables are exposed in the Inspector:

Variable Default Description
auto_reconnect true Enable/disable automatic reconnection
max_reconnect_attempts 5 Give up after this many attempts
reconnect_base_delay 1.0 Delay multiplied by attempt count (1 s, 2 s, 3 s…)

A new signal reconnect_attempt(attempt: int, max: int) is emitted on each retry so the game UI can show feedback like "Reconnecting 2/5…".

On a successful reconnection _reconnect_attempts resets to 0 at both success paths (polling in _on_open, WebSocket in _on_pong). If all attempts are exhausted, engine_close() is called normally and engine_connection_closed fires once.

- Implement reconnect logic
- Handle _on_noop
@garciaf garciaf changed the title [FEATURE] Implement reconnect and noop handler ## Bug fixes and reconnection for godot-socketio Apr 5, 2026
@garciaf garciaf marked this pull request as ready for review April 5, 2026 06:22
@garciaf garciaf changed the title ## Bug fixes and reconnection for godot-socketio Handle reconnection for godot-socketio Apr 5, 2026
@garciaf
Copy link
Copy Markdown
Author

garciaf commented Apr 6, 2026

Thanks again for this amazing addon @msabaeian, thanks to you, I could make my card game so much more reliable.
It worked like a charm.

If this PR makes sense to you, I am really happy to contribute; otherwise, I will remain on the fork I have.

Thanks again for taking care of such a complicated problem

@msabaeian
Copy link
Copy Markdown
Owner

Hey @garciaf! thanks for your work! I’ll review this in the coming week and if everything looks good, merge it.
sorry for the late response I kinda lost track of this repo

@msabaeian msabaeian self-requested a review April 16, 2026 12:04
@garciaf
Copy link
Copy Markdown
Author

garciaf commented Apr 20, 2026

All good @msabaeian if you want to review it in the game I am working on, I can give you further access to the prototype I have.

But basically, I use this plugin to allow people to play the Godot game on their phone (With a web browser).

And Socket IO is so much more reliable than Websocket. Your work is incredible.
Also, I fixed a small typo in this PR to allow working with Godot 4.6

Copy link
Copy Markdown
Owner

@msabaeian msabaeian left a comment

Choose a reason for hiding this comment

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

thanks for your effort, i've some question and suggestions


var session_id: String = ""
var state: State = State.DISCONNECTED
var state = State.DISCONNECTED
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Question: why removing state type?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@msabaeian This was failing on Godot 4.6
maybe I could try

var state := State.DISCONNECTED

But Godot 4.6 would fail at this point. I would need to investigate the nature of the error

@export var path: String = "/engine.io"
@export var auto_reconnect: bool = true
@export var max_reconnect_attempts: int = 5
@export var reconnect_base_delay: float = 1.0
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nit: it would be good to mention type of this value, seconds or milliseconds

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Very good point.

would

@export var reconnect_base_delay_seconds: float = 1.0

Be what you think? Do you have some conventions in the project? When it comes to defined durations?

signal engine_connection_closed()
signal engine_message_received(data: String)
signal engine_transport_upgraded()
signal reconnect_attempt(attempt: int, max: int)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Suggested change
signal reconnect_attempt(attempt: int, max: int)
signal reconnection_attempted(attempt: int, max: int)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Very good

engine_close()
if auto_reconnect and _reconnect_attempts < max_reconnect_attempts:
_reconnect_attempts += 1
reconnect_attempt.emit(_reconnect_attempts, max_reconnect_attempts)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Question: wouldn't it better to emit this after attempting not before it? not sure what's the behavior in socketio library itself

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

We could indeed but since everything is wired via signal we technically do not know when this is done. But I could put right after t.start()
That's what you have in mind right?

Comment on lines +81 to +91
session_id = ""
state = State.DISCONNECTED
_websocket = null
_polling_http_request = null
_send_data_http_request = null
_send_data_queue.clear()
_probe_sent = false
_transport_type = TransportType.POLLING
_ping_interval = 0
_pong_timeout = 0
_max_payload = 0
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Suggested change
session_id = ""
state = State.DISCONNECTED
_websocket = null
_polling_http_request = null
_send_data_http_request = null
_send_data_queue.clear()
_probe_sent = false
_transport_type = TransportType.POLLING
_ping_interval = 0
_pong_timeout = 0
_max_payload = 0
_clear_values()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Very very convenient! Noiceee

disconnect_namespace(ns)

engine_close()
socket_disconnected.emit()
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

nice catch!


func _on_pong():
func _on_pong(payload: String = ""):
if payload != "probe":
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

i don't think socketio ever sends a pong packet which doesn't have probe, but let's add a warning here in case of those unexpected scenarios

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not a failing one, right? Just a warning in the console?
What do you use usually?

@garciaf
Copy link
Copy Markdown
Author

garciaf commented May 4, 2026

@msabaeian thank you so much for looking into it and very good comment. I will fix it and push it.

We might also consider chaning the Readme afterward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants