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

  1. Configure the server to log to stderr only (most MCP SDKs default to stderr; redirect any stdout logging).
  2. Use a server that implements Content-Length framing per the MCP stdio spec.
  3. 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

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


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