Hmbown/CodeWhale · error

Stdio transport read error: {err} {stderr}

Error message

Stdio transport read error: {err}
{stderr}

What it means

Reading the server's stdout failed inside read_line_capped() — an I/O error (pipe broken because the child died) or a line exceeding MAX_MCP_RESPONSE_BYTES (16 MiB) with no newline. The message appends the server's recent stderr from a ring-buffered tail, since a crashed child process is the usual root cause and stderr normally says why.

Source

Thrown at crates/tui/src/mcp/stdio.rs:318

        msg.push(b'\n');
        self.stdin.write_all(&msg).await?;
        self.stdin.flush().await?;
        Ok(())
    }

    async fn recv(&mut self) -> Result<Vec<u8>> {
        let mut line_bytes: Vec<u8> = Vec::new();
        loop {
            // Bounded read: a server emitting a newline-free multi-GB "line"
            // must not OOM us (read_line is unbounded).
            let bytes =
                match read_line_capped(&mut self.reader, &mut line_bytes, MAX_MCP_RESPONSE_BYTES)
                    .await
                {
                    Ok(b) => b,
                    Err(err) => {
                        if let Some(stderr) = format_stderr_context(&self.stderr_tail).await {
                            anyhow::bail!("Stdio transport read error: {err}\n{stderr}");
                        }
                        return Err(err.into());
                    }
                };
            if bytes == 0 {
                if let Some(stderr) = format_stderr_context(&self.stderr_tail).await {
                    anyhow::bail!("Stdio transport closed\n{stderr}");
                }
                anyhow::bail!("Stdio transport closed");
            }

            let line = String::from_utf8_lossy(&line_bytes);
            let trimmed = line.trim();
            if trimmed.is_empty() {
                continue;
            }

            return Ok(trimmed.as_bytes().to_vec());

View on GitHub (pinned to 8880682c63)

Solutions

  1. Read the stderr tail included after the first line of the message — it usually contains the crash reason
  2. Run the exact command and args from the server config in a shell and reproduce the failure
  3. If the cap was hit: the server must emit newline-delimited JSON with lines under 16 MiB
  4. If stderr is silent, check dmesg/journalctl for OOM or signal kills
Defensive patterns

Strategy: try-catch

Type guard

```rust
fn has_stderr_context(msg: &str) -> bool {
    msg.starts_with("Stdio transport read error") && msg.contains('\n')
}
```

Try / catch

```rust
match stdio_transport.recv().await {
    Err(e) => {
        let msg = format!("{e:#}");
        if has_stderr_context(&msg) {
            show_server_failure(&msg); // stderr tail is embedded after the first line
        }
        return Err(e);
    }
    frame => frame?,
}
```

Prevention

When it happens

Trigger: The spawned MCP server dies or closes stdout mid-session (EIO on the pipe), or emits a single newline-free line larger than 16 MiB (e.g. a minified blob), tripping the capped read.

Common situations: Server crashes on a malformed request; npx/node child killed by the OOM killer; server printing huge single-line payloads.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/ae81b9813dccdc98. Report an issue: GitHub.