Kuberwastaken/claurst · error · anyhow::Error
LSP server closed stdout
Error message
LSP server closed stdout
What it means
The LSP client's framing reader (`read_message`, driven by `start`) does a blocking `read_line` on the language-server's stdout and treats a zero-byte read as end-of-stream. Reaching EOF means the LSP server process terminated (or closed its stdout) mid-protocol, so no message can be read and the client aborts with this error.
Solutions
- Run the language-server command from the error/config manually in a terminal to see why it exits (bad path, missing args, immediate crash).
- Check the server's stderr output — LSP servers log startup errors there; fix the underlying startup problem (binary path, version, workspace root).
- Verify the server process stays alive (`ps aux | grep <server>`) while the client is running; if it dies, capture its exit code/stderr.
- Update or reinstall the language server if it crashes on the current project.
Example fix
// before: server dies, stdout EOF
"rust-analyzer": { "command": "rust-analyser" } // typo, exec fails and process exits
// after
"rust-analyzer": { "command": "rust-analyzer" } Defensive patterns
Strategy: try-catch
Validate before calling
// before starting the LSP session, confirm the server binary exists and runs:
let status = std::process::Command::new(&server_cmd)
.arg("--version")
.status()
.map_err(|e| anyhow::anyhow!("LSP server `{}` not runnable: {e}", server_cmd.display()))?;
if !status.success() {
anyhow::bail!("LSP server `--version` exited with {status}");
} Type guard
fn server_binary_available(cmd: &str) -> bool {
std::process::Command::new(cmd)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
} Try / catch
match lsp.start().await {
Err(e) if e.to_string() == "LSP server closed stdout" => {
eprintln!("Language server died before responding. Check its stderr and binary path: {e:#}");
// inspect server stderr / restart server
}
other => other?,
} Prevention
- Verify the LSP binary path resolves (`which <server>`) before configuring it.
- Keep language servers updated; old versions may crash on newer project files.
- Watch server stderr — most crashes are logged there before stdout closes.
- Test the server standalone with `--version` or a minimal initialize request before wiring it into the client.
- Monitor resource limits (OOM kills are a common cause of silent server death).
When it happens
Trigger: Any `read_message` call after the language server's stdout hits EOF: the server process crashed, exited due to a startup failure (bad executable, missing workspace root, unsupported flags), or was killed externally while the client awaited a response.
Common situations: The configured LSP binary path is wrong or the binary exits immediately; the server panics on a malformed request; the machine runs out of memory and the OOM killer reaps the server; a shell wrapper script exits instead of exec-ing the server.
Related errors
- LSP message missing Content-Length header
- LSP request ' ' channel closed (server: )
- LSP server stdin not available
- LSP server stdout not available
- Bridge session registration failed: authentication error
AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10).
Data as JSON: /api/errors/c39dfa671c11f5d3.
Report an issue: GitHub.
Appendix: source
Thrown at src-rust/crates/core/src/lsp.rs:145
writer: &mut BufWriter<ChildStdin>,
body: &str,
) -> anyhow::Result<()> {
let header = format!("Content-Length: {}\r\n\r\n", body.len());
writer.write_all(header.as_bytes()).await?;
writer.write_all(body.as_bytes()).await?;
writer.flush().await?;
Ok(())
}
async fn read_message(
reader: &mut BufReader<ChildStdout>,
) -> anyhow::Result<serde_json::Value> {
let mut content_length: usize = 0;
loop {
let mut line = String::new();
let n = reader.read_line(&mut line).await?;
if n == 0 {
return Err(anyhow::anyhow!("LSP server closed stdout"));
}
let trimmed = line.trim_end_matches(['\r', '\n']);
if trimmed.is_empty() {
break;
}
if let Some(val) = trimmed.strip_prefix("Content-Length: ") {
content_length = val.trim().parse()?;
}
}
if content_length == 0 {
return Err(anyhow::anyhow!("LSP message missing Content-Length header"));
}
let mut buf = vec![0u8; content_length];
reader.read_exact(&mut buf).await?;
Ok(serde_json::from_slice(&buf)?)
}
// ---------------------------------------------------------------------------
View on GitHub (pinned to b0637c97ec)