zeroclaw-labs/zeroclaw · error · anyhow::Error

IRC connection closed by server

Error message

IRC connection closed by server

What it means

In IRC listen's read loop, BufReader::read_line returning 0 bytes means the server closed the TLS stream (EOF); the loop bails immediately. It sits right after a READ_TIMEOUT wrapper, so this error specifically means data ended (FIN/RST), not silence. IRC servers close connections for ping-timeout (client failed PONG), KILL, excess flood, netsplits, or restarts — the caller is expected to treat listen's error as 'reconnect'.

Source

Thrown at crates/zeroclaw-channels/src/irc.rs:479

        loop {
            line.clear();
            let n = tokio::time::timeout(READ_TIMEOUT, buf_reader.read_line(&mut line))
                .await
                .map_err(|_| {
                    ::zeroclaw_log::record!(
                        WARN,
                        ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Timeout)
                            .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                            .with_attrs(::serde_json::json!({
                                "timeout": format!("{:?}", READ_TIMEOUT),
                            })),
                        "irc: read timed out"
                    );
                    anyhow::Error::msg(format!("IRC read timed out (no data for {READ_TIMEOUT:?})"))
                })??;
            if n == 0 {
                anyhow::bail!("IRC connection closed by server");
            }

            let Some(msg) = IrcMessage::parse(&line) else {
                continue;
            };

            match msg.command.as_str() {
                "PING" => {
                    let token = msg.params.first().map_or("", String::as_str);
                    let mut guard = self.writer.lock().await;
                    if let Some(ref mut w) = *guard {
                        Self::send_raw(w, &format!("PONG :{token}")).await?;
                    }
                }

                // CAP responses for SASL
                "CAP" if sasl_pending && msg.params.iter().any(|p| p.contains("sasl")) => {
                    if msg.params.iter().any(|p| p.contains("ACK")) {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Treat this error as reconnect-worthy in the supervisor: back off briefly and re-enter listen() (registration, SASL, JOIN re-run)
  2. If disconnects recur quickly, check whether the server sent an ERROR/KILL line just before EOF (enable raw line logging) and address that reason
  3. Keep-alive harden: ensure PING/PONG handling stays responsive (the loop answers PING with PONG) and consider a lightweight periodic activity source if the network drops idle clients
  4. Use exponential backoff on repeated immediate EOFs to avoid hammering the server
Defensive patterns

Strategy: retry

Try / catch

loop {
    if let Err(e) = irc.listen(tx.clone()).await {
        if e.to_string().contains("closed by server") || e.to_string().contains("read timed out") {
            tokio::time::sleep(backoff.next()).await; // reconnect: registration + JOIN re-run
            continue;
        }
        return Err(e); // e.g. 464 password mismatch: terminal, do not loop
    }
}

Prevention

When it happens

Trigger: Any point during listen(): the server sends FIN after the client missed PING/PONG cadence (e.g. process paused, NAT idle timeout dropped the mapping), an operator KILLs the client, the nick collides terminally, or the IRCd restarts. read_line returns Ok(0) and the bail fires before IrcMessage parsing.

Common situations: Long-running bots behind NAT/firewalls that drop idle TLS sessions; laptop sleep/resume resuming a dead socket; aggressive anti-idle server settings; scheduled IRCd maintenance.

Understand the failure class

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/140361fc4eecefc6. Report an issue: GitHub.