ultraworkers/claw-code · error · std::io::Error
MCP stdio stream closed while reading headers
Error message
MCP stdio stream closed while reading headers
What it means
Client-side `McpStdioProcess::read_frame` (runtime/src/mcp_stdio.rs:1241): EOF (0 bytes from read_line) while still inside the Content-Length header loop of an LSP-style frame on the server's stdout — the server died mid-frame. Note this loop only breaks on the EXACT string `"\r\n"`; a server that terminates its header block with a bare `"\n"` never breaks, reads to EOF, and produces this same error despite 'complete' framing.
Source
Thrown at rust/crates/runtime/src/mcp_stdio.rs:1241
let mut buffer = vec![0_u8; 4096];
let read = self.stdout.read(&mut buffer).await?;
buffer.truncate(read);
Ok(buffer)
}
pub async fn write_frame(&mut self, payload: &[u8]) -> io::Result<()> {
let encoded = encode_frame(payload);
self.write_all(&encoded).await?;
self.flush().await
}
pub async fn read_frame(&mut self) -> io::Result<Vec<u8>> {
let mut content_length = None;
loop {
let mut line = String::new();
let bytes_read = self.stdout.read_line(&mut line).await?;
if bytes_read == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"MCP stdio stream closed while reading headers",
));
}
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);
}
}
}View on GitHub (pinned to 08106b0c37)
Solutions
- Check the server's stderr and exit status for the crash cause; fix the crash first.
- If you author the server, emit strict CRLF (`\r\n\r\n` after headers) — LF-only framing is not recognized by this reader.
- Handle the error by restarting or disabling the server connection, not by retrying the same read.
Example fix
# before (server writes LF-only framing)
printf 'Content-Length: 18\n\n{"jsonrpc":"2.0"...}' # client hangs, then: stream closed while reading headers
# after
printf 'Content-Length: 18\r\n\r\n{"jsonrpc":"2.0"...}' Defensive patterns
Strategy: fallback
Try / catch
match process.read_frame().await {
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
// server died mid-header (or used bare-\n framing): mark server unhealthy,
// log stderr, restart or disable the connection
}
other => other,
} Prevention
- If you author the server: terminate header blocks with CRLF `\r\n\r\n` — this reader ignores bare `\n`
- Write frames atomically (single write, then flush)
- Capture the child's stderr so mid-frame crashes are diagnosable
When it happens
Trigger: Server process crashing after writing partial headers; server killed mid-response; a server emitting LF-only line endings in its framing (bare `\n` blank line does not terminate the header loop here, unlike the server-side reader which accepts both `\r\n` and `\n`).
Common situations: Debugging homegrown MCP servers that use `\n` framing; servers OOM-killed while streaming a large tools/list response; OOM or panic mid-write.
Related errors
- MCP stdio stream closed while reading headers
- missing Content-Length header
- MCP stdio stream closed while reading line
- missing Content-Length header
- MCP response for {method} used unsupported jsonrpc version `
AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18).
Data as JSON: /api/errors/0924431b46bfcfbc.
Report an issue: GitHub.