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
- Reconnect: drop the current client/connection and call `connect` again (the handshake will re-run against the new daemon instance).
- 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.
- 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.
- 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
- Wrap long-lived uplink sessions in a supervisor loop that reconnects on ConnectionLost with exponential backoff.
- Monitor the daemon process (health check) so restarts are known before RPCs fail.
- Keep requests short-lived; re-establish the connection after daemon upgrades instead of reusing old sockets.
- Log the chained cause to distinguish clean EOF (restart) from reset/broken pipe.
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
- Connection failures: ECONNREFUSED, ECONNRESET, and friends — why connections get refused, reset, or dropped.
Related errors
- failed to fetch {name} from {url} (HTTP {})
- {label} download failed: HTTP {status}
- daemon request: {e}
- daemon request: {e}
- cannot remove capsule authority while an install transaction
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/412556bfd8c1e550.
Report an issue: GitHub.