Hmbown/CodeWhale · error
connection stdin poisoned by an earlier panic
Error message
connection stdin poisoned by an earlier panic
What it means
The MCP stdio client wraps the child server's stdin in an `Arc<Mutex<ChildStdin>>`; the reader thread locks this mutex to write JSON-RPC responses to server-initiated requests (e.g. pings). Rust mutexes are poisoned when a holder panics, so `lock()` returns Err and this error is raised: some earlier code path panicked while holding the stdin lock, and the client refuses to guess whether the child's stdin is still coherent. The failure is reported back as an Invalid message on the response channel and the reader loop stops.
Solutions
- Find the original panic: it happened while the stdin mutex was held — check logs/backtraces around `write_jsonrpc_line` and request sends; fix that panic (e.g. handle broken-pipe write errors instead of unwrapping).
- Restart the MCP server connection (re-spawn via `spawn_with_timeouts`); a poisoned mutex is not recoverable in-process for that child.
- Check the server binary for crashes mid-protocol: a child dying while stdin is locked often precipitates the panicking write path.
- If you own the client code, switch lock sites to `lock().unwrap_or_else(|p| p.into_inner())` only if you can prove the stdin state is still valid, or use a poisoning-free lock (parking_lot) deliberately.
- Verify the JSON-RPC payload passed to `write_jsonrpc_line` is serializable and that no panic hook aborts mid-write.
Example fix
// before: panic propagates and poisons the stdin mutex
stdin.lock().unwrap().write_all(&payload).unwrap();
// after: propagate the error instead of panicking while holding the lock
let mut guard = stdin.lock().map_err(|_| anyhow!("stdin mutex poisoned"))?;
guard.write_all(&payload).context("write jsonrpc request")?; Defensive patterns
Strategy: try-catch
Validate before calling
// No pre-call check can detect poisoning directly; verify the child is alive // and the connection was spawned cleanly before use. assert!(child_alive(&connection), "MCP server child already exited; respawn before sending requests");
Type guard
fn connection_healthy(conn: &Connection) -> bool {
conn.stdin.as_ref().map_or(false, |s| !s.is_poisoned())
} Try / catch
match connection.request(&server, method, params) {
Err(e) if e.to_string().contains("stdin poisoned") => {
eprintln!("MCP stdin poisoned; re-spawning server '{server}'");
let connection = spawn_with_timeouts(server, ...)?; // recovery = respawn
}
Err(e) => eprintln!("request failed: {e:#}"),
Ok(v) => { /* use response */ }
} Prevention
- Never panic while holding the stdin mutex — propagate write/serialize errors instead.
- Handle broken-pipe writes to a dying child explicitly rather than unwrapping.
- Validate JSON-RPC payloads are serializable Values before sending.
- On any poisoning error, respawn the MCP server process; poisoned state is not recoverable in-process.
- Keep the server binary healthy: monitor child exits and restarts, since a dying child often triggers the panicking write path.
When it happens
Trigger: Calling `spawn_with_timeouts` (or any subsequent request over the spawned stdio connection) while a server-initiated request arrives and `stdin.lock()` fails because a previous lock holder panicked — e.g. a panic inside `write_jsonrpc_line` or the code that held the lock during a client request write.
Common situations: A panic while writing a request to a misbehaving/crashing MCP server (broken pipe handling that unwraps); a bug in the write path (serde serialization panic); concurrency between the request writer and the reader thread's response writer triggering a panic path; running a server binary that closes stdin abruptly.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Expected Ok for scoped npm package via
- Expected Ok, got
- Expected Ok when cwd anchors relative path, got
- Expected Warning for relative path argument, got
- Absolute path should not warn
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/96676a0c42d26391.
Report an issue: GitHub.
Appendix: source
Thrown at crates/mcp/src/stdio_client.rs:596
// client request is in flight. Route them before the
// zero-capacity response channel: blocking there until
// the next client call would violate MCP ping's prompt
// response requirement. Valid notifications need no
// response and likewise must not occupy the rendezvous.
if let Ok(message) = serde_json::from_str::<Value>(&line)
&& message.get("jsonrpc").and_then(Value::as_str) == Some("2.0")
&& message.get("method").and_then(Value::as_str).is_some()
{
let Some(response) = response_to_server_request(&message) else {
continue;
};
let Some(stdin) = response_stdin.upgrade() else {
break;
};
let result = stdin
.lock()
.map_err(|_| {
anyhow!("connection stdin poisoned by an earlier panic")
})
.and_then(|mut stdin| write_jsonrpc_line(&mut *stdin, &response));
// Never retain the temporary strong handle while a
// failure waits on the rendezvous channel. Drop
// must remain able to close child stdin promptly.
drop(stdin);
if let Err(error) = result {
let _ = sender.send(ChildStdoutMessage::Invalid(format!(
"MCP server '{response_server_name}': failed to answer idle child request: {error:#}"
)));
break;
}
continue;
}
if sender.send(ChildStdoutMessage::Line(line)).is_err() {
break;
}
}View on GitHub (pinned to 73e0f67d83)