Skip to content

fix: retry on first connection failure and on failed IRC registration - #413

Open
mbologna wants to merge 1 commit into
kiwiirc:masterfrom
mbologna:fix/reconnect-attempts-initial-value
Open

fix: retry on first connection failure and on failed IRC registration#413
mbologna wants to merge 1 commit into
kiwiirc:masterfrom
mbologna:fix/reconnect-attempts-initial-value

Conversation

@mbologna

Copy link
Copy Markdown

Problem

When an IRC connection fails for the first time (DNS not yet ready, transient network error, ECONNRESET during registration), The Lounge shows:

Disconnected from the network, and will not reconnect. Use /connect to reconnect again.

Even though auto_reconnect is enabled and auto_reconnect_max_retries is set to a high value (e.g. 30 in The Lounge). This has been reported in several places:

Root cause

The reconnect decision in socketClose is:

} else if (that.reconnect_attempts && that.reconnect_attempts < that.auto_reconnect_max_retries) {
    should_reconnect = true;

// If we were originally connected OK, reconnect
} else if (was_connected && safely_registered) {
    should_reconnect = true;
} else {
    should_reconnect = false;
}

Bug 1 — initial value: reconnect_attempts is initialised to 0. Since 0 is falsy in JavaScript, the first branch never fires on a first-ever connection failure. was_connected is also false, so the second branch doesn't fire either. Result: the very first failure gives up immediately, no matter how high max_retries is.

Bug 2 — premature reset: reconnect_attempts is reset to 0 inside socketOpen (raw TCP connect). This means a connection that reached the TCP layer but failed IRC registration (auth timeout, server-side ECONNRESET during NICK/USER, netsplit during connect) will reset the counter on the next attempt's TCP connect, effectively giving only one IRC-level retry before exhausting the counter.

Both bugs can be observed in the log pattern from #4103:

Reconnecting in 11 seconds… (Attempt 4)   ← TCP connects
Connected to the network.                  ← socketOpen fires, resets counter to 0
-server- *** Found your hostname
Connection closed unexpectedly: ECONNRESET ← socketClose fires, reconnect_attempts == 0 → give up
Disconnected from the network, and will not reconnect.

Fix

Three minimal changes, first proposed by @mornfall in this issue:

  1. Initialise reconnect_attempts to 1 — the first failure now satisfies reconnect_attempts > 0 && reconnect_attempts < max_retries, triggering a retry.

  2. Remove the reset from socketOpen — a raw TCP connection is not a successful IRC session. The counter should not reset until IRC registration completes.

  3. Reset to 1 in registeredSuccessfully — after a fully successful registration, the counter resets so the next connection problem gets a fresh set of retries.

Validation

@mornfall has been running this exact fix in production for 2+ years with ~100 users without issues (last comment in this thread). The fix has also been independently validated by @tokariu.

Diff

-        this.reconnect_attempts = 0;
+        this.reconnect_attempts = 1;

     registeredSuccessfully() {
         this.registered = Date.now();
+        this.reconnect_attempts = 1;
     }

     function socketOpen() {
         that.debugOut('Socket fully connected');
-        that.reconnect_attempts = 0;
         that.connected = true;

Root cause: the socketClose reconnect gate checks
  reconnect_attempts > 0 && reconnect_attempts < max_retries

But reconnect_attempts is initialised to 0, so the first connection
failure always falls through to "give up" — the condition is falsy.

Additionally, reconnect_attempts is reset in socketOpen (raw TCP
connect), not in registeredSuccessfully. A connection that got a TCP
socket but failed IRC registration (auth timeout, ECONNRESET during
registration, server-side kill) would reset the counter immediately on
the next attempt, exhausting retries before IRC registration ever
succeeds.

Fix (proposed by mornfall in kiwiirc#211, validated with ~100 users for 2+
years without issues):

1. Initialise reconnect_attempts to 1 so the first failure triggers the
   retry branch.
2. Remove the premature reset from socketOpen (TCP connected ≠ IRC
   registered).
3. Reset reconnect_attempts to 1 in registeredSuccessfully so the
   counter starts fresh after a fully successful registration.

Fixes kiwiirc#211
@tokariu

tokariu commented Apr 25, 2026

Copy link
Copy Markdown

I've been running this fix on my instance for years now without issues and the handling of reconnects is much better this way. it's like it should've been from the beginning. so let's add it then.

@Ian-Costa18

Copy link
Copy Markdown

Independent confirmation of the diagnosis here: I hit exactly this on a bouncer with four
networks, where a saturated disk made registration routinely outlast the transport's 150s idle
timeout. Every network died on its first connect and nothing ever retried — verified from the
SOCKS proxy's own per-connection log, which showed zero further attempts for over eight hours.
Your "Bug 2 — premature reset" is precisely it.

One thing I think is unintentional, though. Setting reconnect_attempts to 1 rather than 0
means it is never falsy again, so this branch always wins:

} else if (that.reconnect_attempts && that.reconnect_attempts < that.auto_reconnect_max_retries) {

which makes the branch below it unreachable, and with it safely_registered — the guard against
aKills, where services disconnect a user
just after login instead of the server refusing the connection outright. That is
indistinguishable from a clean connect followed by a random drop, which is why the existing code
treats "dropped within 5s of registering" as a rejection and stops. With this change a client
killed moments after registering will instead retry up to max_retries. Backoff keeps it from
being a tight loop, but it is the case that guard was written for.

I drove the real Connection through a fake transport across seven cases, scoring against the
behaviour this PR intends. As published, six pass and the aKill case fails. Reordering so the
guard can veto first makes it seven:

if (that.requested_disconnect || !that.auto_reconnect) {
    should_reconnect = false;

// Registered, then dropped almost immediately: an aKill rather than a glitch.
} else if (was_connected && that.registered !== false && !safely_registered) {
    should_reconnect = false;

} else if (that.reconnect_attempts < that.auto_reconnect_max_retries) {
    should_reconnect = true;
} else {
    should_reconnect = false;
}

The reconnect_attempts truthiness test is no longer needed once the counter starts at 1, so the
budget check can be the single gate. I'm happy to open this against your branch as a PR, or to
contribute the seven cases as tests if you want them in here.

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.

3 participants