Kuberwastaken/claurst · error · anyhow::Error
LSP message missing Content-Length header
Error message
LSP message missing Content-Length header
What it means
LSP over stdio requires every message to start with a `Content-Length: <n>` header terminated by a blank line. `read_message` (called from `start`) parses headers until the blank line and, if no valid Content-Length was seen (`content_length == 0`), the stream is not a conforming LSP framing stream, so the client errors out rather than attempting an unbounded body read.
Solutions
- Verify the configured command actually speaks LSP over stdio (e.g. run it and check for Content-Length framed output).
- Redirect any non-protocol output to stderr: fix wrapper scripts to use `exec` and ensure the server doesn't print logs/diagnostics on stdout (use `--log-file` or stderr flags).
- Check the server's stderr for a startup error explaining why it never entered LSP mode (bad root URI, missing initialization).
- Confirm the header the server sends matches `Content-Length: <n>` exactly (some misbehaving servers use different casing/spacing — a protocol bug in the server).
Example fix
// before: wrapper pollutes stdout with logs g(language-server --verbose) # verbose output goes to stdout // after exec language-server --log-file /tmp/ls.log # stdout carries only LSP frames
Defensive patterns
Strategy: validation
Validate before calling
// smoke-test that the command speaks LSP over stdio before configuring it:
use std::process::{Command, Stdio};
let mut child = Command::new(&server_cmd)
.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::inherit())
.spawn()?;
// send a minimal initialize request and expect a Content-Length-framed reply
let req = format!("Content-Length: 2\r\n\r\n{{}}");
use std::io::Write;
child.stdin.take().unwrap().write_all(req.as_bytes())?;
let mut first = String::new();
use std::io::BufRead;
std::io::BufReader::new(child.stdout.take().unwrap()).read_line(&mut first)?;
if !first.starts_with("Content-Length:") {
anyhow::bail!("`{}` does not emit LSP framing on stdout", server_cmd.display());
} Type guard
fn is_lsp_framed_output(line: &str) -> bool {
line.starts_with("Content-Length:")
} Try / catch
match lsp.start().await {
Err(e) if e.to_string().contains("missing Content-Length") => {
eprintln!("`{cmd}` did not speak LSP over stdio. Check it isn't printing logs/errors on stdout: {e:#}");
}
other => other?,
} Prevention
- Only register binaries that implement LSP over stdio as language servers.
- Configure servers to log to a file or stderr — never let debug output reach stdout.
- In wrapper scripts use `exec` so no extra output precedes the LSP stream.
- Validate a new server with a manual initialize handshake before integrating it.
When it happens
Trigger: The server's stdout emitted a header block with no `Content-Length:` line, or an empty/unparseable header value that defaulted content_length to 0 — e.g. the process printed plain-text diagnostics or an error banner on stdout instead of LSP-framed JSON-RPC.
Common situations: Configuring a plain CLI tool (or a server that logs to stdout) as the LSP command; a server that failed startup and printed a human-readable error; a wrapper script contaminating stdout with debug output; a non-LSP binary path in the config.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
Related errors
- LSP server closed stdout
- LSP server stdin not available
- LSP server stdout not available
- LSP request ' ' channel closed (server: )
- Bridge session registration failed: authentication error
AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10).
Data as JSON: /api/errors/ad487bb16548672f.
Report an issue: GitHub.
Appendix: source
Thrown at src-rust/crates/core/src/lsp.rs:156
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)?)
}
// ---------------------------------------------------------------------------
// LspClient
// ---------------------------------------------------------------------------
type PendingMap = Arc<DashMap<u64, oneshot::Sender<serde_json::Value>>>;
/// A running LSP client connected to a single server process.
pub struct LspClient {
pub server_name: String,
pub server_config: LspServerConfig,
/// The child process handle; `None` after shutdown.
process: Option<Child>,
View on GitHub (pinned to b0637c97ec)