astrid-runtime/astrid · error · ReadError::ConnectionLost

connection closed before {want_topic}

Error message

connection closed before {want_topic}

What it means

astrid-uplink's `read_until_topic_typed` maps a clean EOF from `read_raw_frame` (`Ok(None)`) to `ReadError::ConnectionLost` with this message. It means the daemon (or whatever peer holds the other end of the local socket) closed the connection before sending the frame for the topic the client was waiting for — typically a daemon restart or a half-open socket. The library deliberately classifies this as connection loss rather than a timeout, so callers can distinguish 'peer went away' from 'peer is slow'.

Source

Thrown at crates/astrid-uplink/src/socket_client.rs:357

        want_topic: &str,
        timeout: std::time::Duration,
    ) -> std::result::Result<serde_json::Value, ReadError> {
        let deadline = tokio::time::Instant::now()
            .checked_add(timeout)
            .unwrap_or_else(tokio::time::Instant::now);
        loop {
            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
            if remaining.is_zero() {
                return Err(ReadError::Timeout);
            }
            let read = tokio::time::timeout(remaining, self.read_raw_frame()).await;
            let frame = match read {
                Ok(Ok(Some(bytes))) => bytes,
                // `read_raw_frame` maps a clean EOF mid-length-prefix to
                // `Ok(None)`: the peer closed the connection (daemon restart /
                // half-open socket), which is a connection-loss, not a timeout.
                Ok(Ok(None)) => {
                    return Err(ReadError::ConnectionLost(anyhow::anyhow!(
                        "connection closed before {want_topic}"
                    )));
                },
                // A read error (reset / broken pipe / over-large frame) is also
                // an unusable connection.
                Ok(Err(e)) => return Err(ReadError::ConnectionLost(e)),
                // The outer `tokio::time::timeout` fired: the deadline elapsed
                // with the connection still open. The broker may simply be slow.
                Err(_) => return Err(ReadError::Timeout),
            };
            let raw: serde_json::Value = match serde_json::from_slice(&frame) {
                Ok(v) => v,
                Err(_) => continue,
            };
            if raw.get("topic").and_then(|t| t.as_str()) == Some(want_topic) {
                return Ok(raw);
            }
        }

View on GitHub (pinned to affd8760f4)

Solutions

  1. Reconnect: drop the current client/connection and call `connect` again (the handshake will re-run against the new daemon instance).
  2. Check whether the astrid daemon process is running and was not restarted during the call (`systemctl status` or equivalent); add reconnect-with-backoff logic around RPCs.
  3. If it happens immediately after connect, verify the daemon version supports the topic being requested — a daemon that closes instead of replying may not implement it.
  4. Inspect daemon logs at the time of the EOF to determine why it closed the stream.

Example fix

// before: single-shot call, dies on daemon restart
let topic = client.read_until_topic("events").await?;

// after: classify and reconnect
match client.read_until_topic("events").await {
    Err(ReadError::ConnectionLost(e)) => {
        client = astrid_uplink::SocketClient::connect(&principal).await?;
        let topic = client.read_until_topic("events").await?;
    }
    other => other?,
}
Defensive patterns

Strategy: retry

Validate before calling

null

Type guard

null

Try / catch

// Rust
match client.read_until_topic(topic).await {
    Err(ReadError::ConnectionLost(e)) => {
        // reconnect with backoff, then retry
        client = SocketClient::connect(&principal).await?;
        client.read_until_topic(topic).await?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling `read_until_topic` (which delegates to `read_until_topic_typed`) when the peer sends EOF instead of the expected length-prefixed frame; also produced for hard read errors (`Ok(Err(e))`: reset, broken pipe, over-large frame) through the same `ConnectionLost` variant.

Common situations: The astrid daemon restarted or crashed mid-request; the socket was half-open after a suspend/network change; a proxy or socket supervisor closed the connection; the daemon rejected the client and dropped the stream before replying.

Understand the failure class

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/412556bfd8c1e550. Report an issue: GitHub.