ultraworkers/claw-code · error · std::io::Error
missing Content-Length header
Error message
missing Content-Length header
What it means
Client-side `McpStdioProcess::read_frame` (runtime/src/mcp_stdio.rs:1262): the header block on the server's stdout terminated (blank `\r\n` line) without any parseable Content-Length header. Lines without a ':' are ignored, so non-protocol output followed by a blank line lands here. `ErrorKind::InvalidData`.
Source
Thrown at rust/crates/runtime/src/mcp_stdio.rs:1262
));
}
if line == "\r\n" {
break;
}
let header = line.trim_end_matches(['\r', '\n']);
if let Some((name, value)) = header.split_once(':') {
if name.trim().eq_ignore_ascii_case("Content-Length") {
let parsed = value
.trim()
.parse::<usize>()
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
content_length = Some(parsed);
}
}
}
let content_length = content_length.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "missing Content-Length header")
})?;
let mut payload = vec![0_u8; content_length];
self.stdout.read_exact(&mut payload).await?;
Ok(payload)
}
pub async fn write_jsonrpc_message<T: Serialize>(&mut self, message: &T) -> io::Result<()> {
let body = serde_json::to_vec(message)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
self.write_frame(&body).await
}
pub async fn read_jsonrpc_message<T: DeserializeOwned>(&mut self) -> io::Result<T> {
let payload = self.read_frame().await?;
serde_json::from_slice(&payload)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
}
View on GitHub (pinned to 08106b0c37)
Solutions
- Configure the server to log to stderr only (most MCP SDKs default to stderr; redirect any stdout logging).
- Use a server that implements Content-Length framing per the MCP stdio spec.
- Reproduce manually: run the server command and inspect stdout vs stderr to find the stray output.
Example fix
# before (server logs to stdout) logger = logging.getLogger(); logger.addHandler(logging.StreamHandler(sys.stdout)) # after (log to stderr so stdout carries only framed JSON-RPC) logger.addHandler(logging.StreamHandler(sys.stderr))
Defensive patterns
Strategy: try-catch
Try / catch
match process.read_frame().await {
Err(e) if e.kind() == io::ErrorKind::InvalidData
&& e.to_string().contains("missing Content-Length") => {
// server is polluting stdout (logs/NDJSON): fix server to use stderr + framed output
}
other => other,
} Prevention
- Route ALL server logging to stderr; stdout must carry only framed JSON-RPC
- Don't add print/banner/debug statements to stdout in MCP servers
- Reproduce by running the server command and diffing stdout vs stderr
When it happens
Trigger: The MCP server logging to STDOUT (a log line, then a blank line) corrupts the frame stream; a server implementing newline-delimited JSON instead of LSP-style Content-Length framing; a startup banner printed to stdout before the first frame; a misspelled Content-Length header name.
Common situations: Python/Node MCP servers whose logger writes to stdout by default; debug prints left in a forked server; versions where the server switched framing mode; wrappers like `npx` banners.
Related errors
- missing Content-Length header
- MCP stdio stream closed while reading headers
- MCP stdio stream closed while reading headers
- MCP stdio stream closed while reading line
- MCP response for {method} used unsupported jsonrpc version `
AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18).
Data as JSON: /api/errors/3b452ce4584fdd52.
Report an issue: GitHub.