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
Server-side MCP frame reader `read_frame` (runtime/src/mcp_server.rs:259): the header loop got at least one header line (`first_header == false`), then `read_line` returned 0 bytes — stdin closed in the middle of a Content-Length header block, before the blank line that terminates it. Clean EOF before ANY bytes returns `Ok(None)` instead; only mid-header EOF errors with `UnexpectedEof`. This is claw acting as an MCP server over stdio.
Source
Thrown at rust/crates/runtime/src/mcp_server.rs:259
}),
}
}
/// Reads a single LSP-framed JSON-RPC payload from `reader`.
///
/// Returns `Ok(None)` on clean EOF before any header bytes have been read,
/// matching how [`crate::mcp_stdio::McpStdioProcess`] treats stream closure.
async fn read_frame(reader: &mut BufReader<Stdin>) -> io::Result<Option<Vec<u8>>> {
let mut content_length: Option<usize> = None;
let mut first_header = true;
loop {
let mut line = String::new();
let bytes_read = reader.read_line(&mut line).await?;
if bytes_read == 0 {
if first_header {
return Ok(None);
}
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"MCP stdio stream closed while reading headers",
));
}
first_header = false;
if line == "\r\n" || line == "\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
- Treat this as fatal for the connection: stop the read loop and let the server task shut down (the peer is gone).
- On the client side, write frames atomically and flush before exiting so headers are never split by termination.
- If you see it on every connect, capture the client's framing code — it is emitting truncated header blocks.
Example fix
// before (client, partial write then exit)
write_all(b"Content-Length: 42\r\n").await;
process::exit(0); // server: stream closed while reading headers
// after (client writes the full frame, then closes)
write_all(format!("Content-Length: {len}\r\n\r\n").as_bytes()).await;
write_all(&body).await;
flush().await; // clean EOF later -> server gets Ok(None) Defensive patterns
Strategy: fallback
Try / catch
match read_frame(&mut reader).await {
Ok(None) => break, // clean EOF before headers: session over
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break, // client died mid-header: end session, log at debug
Err(e) => return Err(e),
Ok(Some(frame)) => handle(frame).await?,
} Prevention
- Clients: write each frame with a single write_all + flush, then close stdin only after all frames
- Servers: treat mid-header EOF as connection termination, not a corrupt-state condition
- Never SIGKILL an MCP client mid-write if graceful shutdown is available
When it happens
Trigger: MCP client process crashes or is SIGKILLed after writing partial headers; a client that writes a header line then closes stdin without finishing the frame; abrupt pipe teardown during session shutdown.
Common situations: IDE/editor restart killing the MCP client mid-write; test harnesses closing the pipe to signal shutdown after emitting a partial frame; OOM-killed clients.
Related errors
- MCP stdio stream closed while reading line
- MCP stdio stream closed while reading headers
- missing Content-Length header
- 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/c28eb0b2517de97f.
Report an issue: GitHub.