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

  1. Check whether the LSP server process is still alive before sending; restart the client (spawn a fresh server) when the channel is closed.
  2. Inspect the server's stderr logs to find why the server exited (bad initialization options, missing binary, version incompatibility).
  3. Guard shutdown ordering: cancel or drain pending requests and stop document notifications before dropping the client.
  4. 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

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


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`. Exits

View on GitHub (pinned to 73e0f67d83)