{"record":{"id":"af77311ed3897277","repo":"Hmbown/CodeWhale","slug":"lsp-outbound-channel-closed","errorCode":null,"errorMessage":"LSP outbound channel closed","messagePattern":"LSP outbound channel closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/lsp/client.rs","lineNumber":356,"sourceCode":"    async fn shutdown(&self) {\n        let mut child = self.child.lock().await;\n        if let Some(mut c) = child.take() {\n            let _ = c.start_kill();\n            let _ = c.wait().await;\n        }\n    }\n}\n\n/// Send a JSON value as one Content-Length-framed JSON-RPC message.\nasync fn send_message(tx: &mpsc::Sender<Vec<u8>>, value: &Value) -> Result<()> {\n    let body = serde_json::to_vec(value).context(\"serialize LSP message\")?;\n    let header = format!(\"Content-Length: {}\\r\\n\\r\\n\", body.len());\n    let mut frame = Vec::with_capacity(header.len() + body.len());\n    frame.extend_from_slice(header.as_bytes());\n    frame.extend_from_slice(&body);\n    tx.send(frame)\n        .await\n        .map_err(|_| anyhow!(\"LSP outbound channel closed\"))?;\n    Ok(())\n}\n\n/// Background task that drains the outbound queue and writes each frame to\n/// the LSP server's stdin. Exits cleanly when the channel closes.\nasync fn writer_task(mut stdin: tokio::process::ChildStdin, mut rx: mpsc::Receiver<Vec<u8>>) {\n    while let Some(frame) = rx.recv().await {\n        if stdin.write_all(&frame).await.is_err() {\n            break;\n        }\n        if stdin.flush().await.is_err() {\n            break;\n        }\n    }\n}\n\n/// Background task that parses `Content-Length`-framed JSON-RPC frames from\n/// the LSP server's stdout. Pushes each parsed JSON value to `tx`. Exits","sourceCodeStart":338,"sourceCodeEnd":374,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/lsp/client.rs#L338-L374","documentation":"send_message could not enqueue a frame because the outbound mpsc receiver is gone: the writer task that drains frames into the server's stdin has exited after a stdin write or flush failure. The server's stdin is no longer reachable - it has exited or its pipe broke. This is the send-time observation of the same dead-server condition that appears as channel-closed on the receive side.","triggerScenarios":"Server process exits between requests while a new request is being sent; the writer task hits a write/flush error and stops; client shutdown races an in-flight send, dropping the channel first.","commonSituations":"Crashed or OOM-killed servers, servers exiting on invalid input, teardown sequences that drop the outbound channel while requests are still being issued.","solutions":["Treat it as a dead server: respawn the server and re-send the request on the fresh channel","Reproduce by running the server manually and sending the same request sequence","Check the server's stderr/log for why it stopped reading input","Order shutdown so sends during teardown get a clean 'shutting down' signal instead of a closed channel"],"exampleFix":"// before: send into a dropped channel\nsend_message(&self.tx_outbound, &payload).await?; // Err(outbound channel closed)\n// after: gate on writer liveness, restart on failure\nif self.writer_gone() {\n    anyhow::bail!(\"LSP writer exited; restart the server\");\n}","handlingStrategy":"retry","validationCode":"// Track writer task completion; refuse sends once it has exited\nfn writer_alive(writer: &tokio::task::JoinHandle<()>) -> bool {\n    !writer.is_finished()\n}","typeGuard":null,"tryCatchPattern":"// Same bounded-restart shape as channel-closed: one respawn, then fail\nif let Err(e) = send_message(&tx, &payload).await {\n    if e.to_string().contains(\"outbound channel closed\") {\n        client = restart_server().await?; // re-send once on the fresh channel\n    } else {\n        return Err(e);\n    }\n}","preventionTips":["Mark the client unusable as soon as the writer task finishes","Log why the writer exited (write vs flush error) before the receiver drops","Serialize shutdown so sends during teardown get a clean closing error","Treat server process exit as the authoritative liveness signal"],"tags":["lsp","channel","stdin","process"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}