Hmbown/CodeWhale · error
LSP outbound channel closed
Error message
LSP outbound channel closed
What it means
send_message frames an LSP JSON-RPC message and pushes it onto the mpsc channel consumed by writer_task, which writes frames to the LSP server's stdin. The oneshot-style failure means the receiving end (writer_task / the writer task) is gone, so the server process has exited or the client was shut down. Any queued or in-flight LSP request will fail with this error once the outbound channel has no receiver.
Solutions
- Check whether the LSP server process is still alive before sending; restart the client (spawn a fresh server) when the channel is closed.
- Inspect the server's stderr logs to find why the server exited (bad initialization options, missing binary, version incompatibility).
- Guard shutdown ordering: cancel or drain pending requests and stop document notifications before dropping the client.
- Re-queue in-flight document open/change notifications after a client restart so editor state stays in sync.
Example fix
// before
client.request(method, params).await?;
// after
if !client.is_running() {
client.restart().await?;
}
client.request(method, params).await.map_err(|e| {
eprintln!("LSP send failed, restarting server: {e}");
client.restart().await?;
client.request(method, params).await
})?; Defensive patterns
Strategy: try-catch
Validate before calling
// Rust-ish guard
if !lsp_client.is_running() {
lsp_client = lsp_client.restart().await?;
} Type guard
fn can_send(client: &LspClient) -> bool { client.is_running() && client.server_alive() } Try / catch
match client.request(method, params).await {
Err(e) if e.to_string().contains("LSP outbound channel closed") => {
client.restart().await?;
client.request(method, params).await
}
other => other,
} Prevention
- Check server liveness before each document-sync burst
- Cancel in-flight notifications before client shutdown
- Monitor server stderr for early crash signals
- Re-open documents after any client restart
When it happens
Trigger: Calling client.request, send via request(), open_or_change (document open/change notifications), or spawn_with_timeout paths after the writer_task has exited because the LSP server process died, stdin closed, or the client was dropped/shut down.
Common situations: Language server crashed (OOM, panic, unsupported workspace), server was killed by a timeout, user closed the editor session while background document sync was still sending didOpen/didChange notifications, or a race between shutdown and a queued turn in tests like queued_ordinary_turn_does_not_multiply_engine_goal_continuations.
Related errors
- LSP diagnostics channel closed before publishDiagnostics
- LSP request channel closed
- Compaction lifecycle task ended before acknowledgement
- duplicate LSP Content-Length
- engine channel closed
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/af77311ed3897277.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/lsp/client.rs:530
let _ = c.wait().await;
}
for task in &self.tasks {
task.abort();
}
self.pending.lock().await.clear();
}
}
/// Send a JSON value as one Content-Length-framed JSON-RPC message.
async fn send_message(tx: &mpsc::Sender<Vec<u8>>, value: &Value) -> Result<()> {
let body = serde_json::to_vec(value).context("serialize LSP message")?;
let header = format!("Content-Length: {}\r\n\r\n", body.len());
let mut frame = Vec::with_capacity(header.len() + body.len());
frame.extend_from_slice(header.as_bytes());
frame.extend_from_slice(&body);
tx.send(frame)
.await
.map_err(|_| anyhow!("LSP outbound channel closed"))?;
Ok(())
}
/// Background task that drains the outbound queue and writes each frame to
/// the LSP server's stdin. Exits cleanly when the channel closes.
async fn writer_task(mut stdin: tokio::process::ChildStdin, mut rx: mpsc::Receiver<Vec<u8>>) {
while let Some(frame) = rx.recv().await {
if stdin.write_all(&frame).await.is_err() {
break;
}
if stdin.flush().await.is_err() {
break;
}
}
}
/// Background task that parses `Content-Length`-framed JSON-RPC frames from
/// the LSP server's stdout. Pushes each parsed JSON value to `tx`. ExitsView on GitHub (pinned to 73e0f67d83)