{"record":{"id":"151bb4642ad969cc","repo":"astrid-runtime/astrid","slug":"daemon-closed-the-connection-before-responding","errorCode":null,"errorMessage":"Daemon closed the connection before responding","messagePattern":"Daemon closed the connection before responding","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-uplink/src/admin_client.rs","lineNumber":190,"sourceCode":"            .with_principal(self.caller.to_string());\n        self.inner.send_message(msg).await?;\n\n        let deadline = tokio::time::Instant::now()\n            .checked_add(self.timeout)\n            .unwrap_or_else(tokio::time::Instant::now);\n        loop {\n            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());\n            if remaining.is_zero() {\n                anyhow::bail!(\n                    \"Admin request timed out after {:?} waiting for {want_response}\",\n                    self.timeout\n                );\n            }\n            let read = tokio::time::timeout(remaining, self.inner.read_raw_frame()).await;\n            let frame = match read {\n                Ok(Ok(Some(bytes))) => bytes,\n                Ok(Ok(None)) => {\n                    anyhow::bail!(\"Daemon closed the connection before responding\");\n                },\n                Ok(Err(e)) => return Err(e),\n                Err(_) => {\n                    anyhow::bail!(\n                        \"Admin request timed out after {:?} waiting for {want_response}\",\n                        self.timeout\n                    );\n                },\n            };\n\n            // The host serializes IPC envelopes through `to_guest_bytes`\n            // which strips the `type` tag for `IpcPayload::RawJson`, so\n            // the bytes the uplink sees from the proxy embed the\n            // response directly under `payload` (no `IpcPayload`\n            // wrapper). Match by topic, then deserialize\n            // `AdminKernelResponse` straight out of the `payload` field.\n            let raw: Value = match serde_json::from_slice(&frame) {\n                Ok(v) => v,","sourceCodeStart":172,"sourceCodeEnd":208,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-uplink/src/admin_client.rs#L172-L208","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the daemon's logs at the time of the call to find the crash/panic and fix that root cause.","Reconnect and retry the request once a fresh daemon instance is running.","If the daemon is supervisor-managed, verify restart policy and resource limits (e.g. OOM) and adjust.","Check that the daemon binary was not killed by a deploy/stop script racing with in-flight admin calls."],"exampleFix":"// before: no reconnection on closed connection\nlet reply = admin_client.request(req).await?;\n// after: recreate the client (and connection) on closed-connection errors\nlet reply = match admin_client.request(req).await {\n    Ok(r) => r,\n    Err(e) if e.to_string().contains(\"closed the connection\") => {\n        admin_client = AdminClient::connect(&socket_path).await?;\n        admin_client.request(req).await?\n    }\n    Err(e) => return Err(e),\n};","handlingStrategy":"try-catch","validationCode":"// Confirm the daemon is alive before sending\nif !daemon_process_running() || !socket_path_exists() {\n    restart_daemon()?;\n}","typeGuard":null,"tryCatchPattern":"match admin_client.request(req).await {\n    Ok(resp) => resp,\n    Err(e) if e.to_string().contains(\"closed the connection\") => {\n        // daemon died mid-request: reconnect to a fresh instance and retry\n        admin_client = AdminClient::connect(&socket_path).await?;\n        admin_client.request(req).await?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Run the daemon under a supervisor with automatic restart.","Check daemon logs/OOM events after every closed-connection error.","Avoid stopping/redeploying the daemon while admin requests are in flight.","Monitor daemon memory so OOM kills are caught before clients see disconnects."],"tags":["ipc","connection-closed","daemon","admin"],"backgroundTag":"connection-refused","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}