xai-org/grok-build · error

transport is closed

Error message

transport is closed

What it means

An io::Error of kind NotConnection (NotConnected) returned when a write is attempted on an MCP transport whose writer half has already been closed or dropped. The guarded lock holds Option<Write>; None means the transport was shut down. It surfaces to callers of `start`-related transport write paths.

Source

Thrown at crates/codegen/xai-grok-mcp/src/servers.rs:2519

    W: AsyncWrite + Send + Unpin + 'static,
{
    type Error = std::io::Error;

    fn send(
        &mut self,
        item: TxJsonRpcMessage<RoleClient>,
    ) -> impl Future<Output = Result<(), Self::Error>> + Send + 'static {
        let lock = self.write.clone();
        async move {
            let mut bytes = serde_json::to_vec(&item).map_err(std::io::Error::other)?;
            bytes.push(b'\n');
            let mut guard = lock.lock().await;
            match guard.as_mut() {
                Some(write) => {
                    write.write_all(&bytes).await?;
                    write.flush().await
                }
                None => Err(std::io::Error::new(
                    std::io::ErrorKind::NotConnected,
                    "transport is closed",
                )),
            }
        }
    }

    async fn receive(&mut self) -> Option<RxJsonRpcMessage<RoleClient>> {
        loop {
            let mut line = Vec::new();
            match self.read.read_until(b'\n', &mut line).await {
                Ok(0) => return None, // genuine end-of-stream
                Ok(_) => {}
                Err(e) => {
                    tracing::debug!(
                        server = %self.server_name,
                        error = %e,
                        "MCP stdio read error; closing transport",

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Check session/transport state before writing; stop sending after shutdown is initiated
  2. Re-create the transport/session via start and retry the message
  3. Treat NotConnected as terminal and tear down the session cleanly
  4. Fix shutdown ordering so writers are closed only after pending sends complete

Example fix

// before
transport.write_all(&msg).await?; // NotConnected after shutdown
// after
if transport.is_open() {
    transport.write_all(&msg).await?;
} else {
    return Err(SessionError::Closed);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// check transport liveness before writing
if !session.transport_is_open() {
    return Err(std::io::Error::new(std::io::ErrorKind::NotConnected, "transport is closed"));
}

Type guard

fn is_writable(t: &Option<Transport>) -> bool {
    matches!(t, Some(w) if !w.is_closed())
}

Try / catch

match write_all(&bytes).await {
    Err(e) if e.kind() == std::io::ErrorKind::NotConnected => {
        // transport shut down; restart via start() or drop session
    }
    other => other?,
}

Prevention

When it happens

Trigger: Writing to the MCP transport after the server/session was shut down, after the peer dropped the connection, or before the writer was installed (start not completed).

Common situations: Sending a response/notification after the client disconnected; racing shutdown with an in-flight write; reusing a session handle after close.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/8d2dfea9a3890abe. Report an issue: GitHub.