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

  1. Run the language-server command from the error/config manually in a terminal to see why it exits (bad path, missing args, immediate crash).
  2. Check the server's stderr output — LSP servers log startup errors there; fix the underlying startup problem (binary path, version, workspace root).
  3. Verify the server process stays alive (`ps aux | grep <server>`) while the client is running; if it dies, capture its exit code/stderr.
  4. 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

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


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)