moghtech/komodo · warning · anyhow::Error

Connection closed

Error message

Connection closed

What it means

The websocket receive loop classifies each incoming frame: on a Close frame from the peer, recv_message returns this error to signal the connection was closed gracefully by the remote side. Subsequent logical message delivery is impossible until a reconnect is established.

Solutions

  1. Treat this as terminal for the socket and re-establish the connection (reconnect loop with backoff)
  2. Send periodic Ping frames from the client to keep intermediaries from closing idle connections
  3. Handle the error variant explicitly to distinguish peer-initiated close from protocol errors
  4. After reconnect, re-run any session/terminal channel setup that was tied to the old socket

Example fix

// before
let msg = ws.recv_message().await?;
// after
let msg = match ws.recv_message().await {
  Ok(m) => m,
  Err(e) if e.to_string() == "Connection closed" => {
    ws = reconnect().await?;
    continue;
  }
  Err(e) => return Err(e),
};
Defensive patterns

Strategy: retry

Try / catch

async fn recv_with_reconnect(ws: &mut Ws) -> anyhow::Result<Message> {
  loop {
    match ws.recv_message().await {
      Ok(m) => return m,
      Err(e) if e.to_string() == "Connection closed" => {
        *ws = connect_with_backoff().await?;
        on_reconnected();
      }
      Err(e) => return Err(e),
    }
  }
}

Prevention

When it happens

Trigger: The remote endpoint sends a websocket Close frame while recv_message is awaiting the next message; e.g. the server shutting down, a reverse-proxy idle timeout, or the peer explicitly closing the session.

Common situations: Long-lived terminal/agent connections dropped after proxy idle timeouts; server restarts during deploys; the remote application calling close() after finishing its work.

Understand the failure class

Related errors


AI-assisted analysis of moghtech/komodo@780ac68b99 (2026-09-08). Data as JSON: /api/errors/084dded5595b0aa6. Report an issue: GitHub.

Appendix: source

Thrown at lib/transport/src/websocket/mod.rs:92

    &mut self,
  ) -> MaybeWithTimeout<
    impl Future<Output = anyhow::Result<TransportMessage>> + Send,
  > {
    MaybeWithTimeout::new(async {
      loop {
        match tokio::time::timeout(
          Duration::from_secs(10),
          self.recv_inner(),
        )
        .await
        .context("Timed out waiting for Ping")??
        {
          WebsocketMessage::Message(message) => {
            return message.decode();
          }
          WebsocketMessage::Ping => continue,
          WebsocketMessage::Close => {
            return Err(anyhow!("Connection closed"));
          }
          WebsocketMessage::Closed => {
            return Err(anyhow!("Connection already closed"));
          }
        }
      }
    })
  }
}

impl<W: Websocket> WebsocketExt for W {}

/// Traits for split websocket receiver
pub trait WebsocketSender {
  /// Streamlined pinging
  fn ping(
    &mut self,
  ) -> impl Future<Output = anyhow::Result<()>> + Send;

View on GitHub (pinned to 780ac68b99)