{"record":{"id":"ae81b9813dccdc98","repo":"Hmbown/CodeWhale","slug":"stdio-transport-read-error-err-stderr","errorCode":null,"errorMessage":"Stdio transport read error: {err}\n{stderr}","messagePattern":"Stdio transport read error: (.+?)\n(.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp/stdio.rs","lineNumber":318,"sourceCode":"        msg.push(b'\\n');\n        self.stdin.write_all(&msg).await?;\n        self.stdin.flush().await?;\n        Ok(())\n    }\n\n    async fn recv(&mut self) -> Result<Vec<u8>> {\n        let mut line_bytes: Vec<u8> = Vec::new();\n        loop {\n            // Bounded read: a server emitting a newline-free multi-GB \"line\"\n            // must not OOM us (read_line is unbounded).\n            let bytes =\n                match read_line_capped(&mut self.reader, &mut line_bytes, MAX_MCP_RESPONSE_BYTES)\n                    .await\n                {\n                    Ok(b) => b,\n                    Err(err) => {\n                        if let Some(stderr) = format_stderr_context(&self.stderr_tail).await {\n                            anyhow::bail!(\"Stdio transport read error: {err}\\n{stderr}\");\n                        }\n                        return Err(err.into());\n                    }\n                };\n            if bytes == 0 {\n                if let Some(stderr) = format_stderr_context(&self.stderr_tail).await {\n                    anyhow::bail!(\"Stdio transport closed\\n{stderr}\");\n                }\n                anyhow::bail!(\"Stdio transport closed\");\n            }\n\n            let line = String::from_utf8_lossy(&line_bytes);\n            let trimmed = line.trim();\n            if trimmed.is_empty() {\n                continue;\n            }\n\n            return Ok(trimmed.as_bytes().to_vec());","sourceCodeStart":300,"sourceCodeEnd":336,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp/stdio.rs#L300-L336","documentation":"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.","triggerScenarios":"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.","commonSituations":"Server crashes on a malformed request; npx/node child killed by the OOM killer; server printing huge single-line payloads.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"```rust\nfn has_stderr_context(msg: &str) -> bool {\n    msg.starts_with(\"Stdio transport read error\") && msg.contains('\\n')\n}\n```","tryCatchPattern":"```rust\nmatch stdio_transport.recv().await {\n    Err(e) => {\n        let msg = format!(\"{e:#}\");\n        if has_stderr_context(&msg) {\n            show_server_failure(&msg); // stderr tail is embedded after the first line\n        }\n        return Err(e);\n    }\n    frame => frame?,\n}\n```","preventionTips":["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"],"tags":["mcp","stdio","subprocess","io"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}