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

  1. Check the server's stderr and exit status for the crash cause; fix the crash first.
  2. If you author the server, emit strict CRLF (`\r\n\r\n` after headers) — LF-only framing is not recognized by this reader.
  3. 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

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


AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18). Data as JSON: /api/errors/0924431b46bfcfbc. Report an issue: GitHub.