astrid-runtime/astrid · error

Daemon closed the connection before responding

Error message

Daemon closed the connection before responding

What it means

While waiting for a response frame in send_and_wait, a None from read_raw_frame means the daemon closed its end of the connection without sending the awaited response. The library surfaces this as a distinct error so callers do not mistake a dead connection for a timeout and can immediately reconnect or report daemon death.

Source

Thrown at crates/astrid-uplink/src/admin_client.rs:190

            .with_principal(self.caller.to_string());
        self.inner.send_message(msg).await?;

        let deadline = tokio::time::Instant::now()
            .checked_add(self.timeout)
            .unwrap_or_else(tokio::time::Instant::now);
        loop {
            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
            if remaining.is_zero() {
                anyhow::bail!(
                    "Admin request timed out after {:?} waiting for {want_response}",
                    self.timeout
                );
            }
            let read = tokio::time::timeout(remaining, self.inner.read_raw_frame()).await;
            let frame = match read {
                Ok(Ok(Some(bytes))) => bytes,
                Ok(Ok(None)) => {
                    anyhow::bail!("Daemon closed the connection before responding");
                },
                Ok(Err(e)) => return Err(e),
                Err(_) => {
                    anyhow::bail!(
                        "Admin request timed out after {:?} waiting for {want_response}",
                        self.timeout
                    );
                },
            };

            // The host serializes IPC envelopes through `to_guest_bytes`
            // which strips the `type` tag for `IpcPayload::RawJson`, so
            // the bytes the uplink sees from the proxy embed the
            // response directly under `payload` (no `IpcPayload`
            // wrapper). Match by topic, then deserialize
            // `AdminKernelResponse` straight out of the `payload` field.
            let raw: Value = match serde_json::from_slice(&frame) {
                Ok(v) => v,

View on GitHub (pinned to affd8760f4)

Solutions

  1. Inspect the daemon's logs at the time of the call to find the crash/panic and fix that root cause.
  2. Reconnect and retry the request once a fresh daemon instance is running.
  3. If the daemon is supervisor-managed, verify restart policy and resource limits (e.g. OOM) and adjust.
  4. Check that the daemon binary was not killed by a deploy/stop script racing with in-flight admin calls.

Example fix

// before: no reconnection on closed connection
let reply = admin_client.request(req).await?;
// after: recreate the client (and connection) on closed-connection errors
let reply = match admin_client.request(req).await {
    Ok(r) => r,
    Err(e) if e.to_string().contains("closed the connection") => {
        admin_client = AdminClient::connect(&socket_path).await?;
        admin_client.request(req).await?
    }
    Err(e) => return Err(e),
};
Defensive patterns

Strategy: try-catch

Validate before calling

// Confirm the daemon is alive before sending
if !daemon_process_running() || !socket_path_exists() {
    restart_daemon()?;
}

Try / catch

match admin_client.request(req).await {
    Ok(resp) => resp,
    Err(e) if e.to_string().contains("closed the connection") => {
        // daemon died mid-request: reconnect to a fresh instance and retry
        admin_client = AdminClient::connect(&socket_path).await?;
        admin_client.request(req).await?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling AdminClient::request or request_agent_derive when the daemon process exits or drops the admin connection mid-request — crash, panic, OOM kill, or deliberate shutdown — before writing the response frame.

Common situations: Daemon crashed while handling this request (check daemon logs/core dump); daemon was restarted or stopped by a supervisor during the call; OS OOM-killer terminated the daemon; connection accepted then closed because the daemon hit a fatal internal error.

Understand the failure class

Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.

Related errors


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