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
- Treat this error as reconnect-worthy in the supervisor: back off briefly and re-enter listen() (registration, SASL, JOIN re-run)
- If disconnects recur quickly, check whether the server sent an ERROR/KILL line just before EOF (enable raw line logging) and address that reason
- 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
- 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
- Wrap listen() in a reconnect supervisor with exponential backoff; EOF is expected over long uptimes
- Keep PONG responses prompt (the loop already answers PING) and avoid blocking the task between ticks
- Distinguish EOF (retry) from auth numerics like 464 (fail fast) in your error handling
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
- Connection failures: ECONNREFUSED, ECONNRESET, and friends — why connections get refused, reset, or dropped.
Related errors
- QQ WebSocket connection closed: stream ended unexpectedly
- QQ WebSocket connection closed: heartbeat ACK timeout ({MAX_
- QQ WebSocket connection closed: write failed
- exceeded {} reconnect attempts, giving up
- channel does not support room creation
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/140361fc4eecefc6.
Report an issue: GitHub.