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
- Read the stderr tail included after the first line of the message — it usually contains the crash reason
- Run the exact command and args from the server config in a shell and reproduce the failure
- If the cap was hit: the server must emit newline-delimited JSON with lines under 16 MiB
- 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
- Run the configured command and args in a shell before enabling the server
- Keep individual stdio lines newline-delimited and under the client's 16 MiB response cap
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
- Stdio transport closed {stderr}
- Stdio transport closed
- Failed to write MCP config {}: {}
- failed to read git range {commit_range!r}: {exc}
- measurement command failed
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/ae81b9813dccdc98.
Report an issue: GitHub.