BloopAI/vibe-kanban · error

Relay control channel closed

Error message

Relay control channel closed

What it means

The relay client select! loop reads inbound streams from the yamux control session. When session.next() returns None, the yamux session (control channel) has been closed by the peer or locally, so no more inbound streams can arrive; the client then errors with 'Relay control channel closed' instead of hanging.

Source

Thrown at crates/relay-tunnel-core/src/client.rs:65

    let ws_io = tungstenite_ws_stream_io(ws_stream);
    let mut session = Session::new_client(ws_io, yamux_config());
    let mut control = session.control();

    tracing::debug!("Relay control channel connected");

    let shutdown = config.shutdown;
    let local_addr = config.local_addr;

    loop {
        tokio::select! {
            _ = shutdown.cancelled() => {
                control.close().await;
                return Ok(());
            }
            inbound = session.next() => {
                let stream = inbound
                    .ok_or_else(|| anyhow::anyhow!("Relay control channel closed"))?
                    .map_err(|e| anyhow::anyhow!("Relay yamux session error: {e}"))?;

                tokio::spawn(async move {
                    if let Err(error) = handle_inbound_stream(stream, local_addr).await {
                        tracing::warn!(?error, "Relay stream handling failed");
                    }
                });
            }
        }
    }
}

async fn handle_inbound_stream(
    stream: tokio_yamux::StreamHandle,
    local_addr: SocketAddr,
) -> anyhow::Result<()> {
    let io = TokioIo::new(stream);

View on GitHub (pinned to 4deb7eca8f)

Solutions

  1. Restart/reconnect: call start_relay again to establish a fresh control channel (implement automatic reconnection with backoff in the caller).
  2. Check the relay server is running and reachable; inspect server logs for why it closed the session.
  3. Ensure keepalive/heartbeat settings on the yamux session and underlying transport are configured to survive idle periods.
  4. Verify client and relay server versions are compatible.

Example fix

// before
let stream = inbound.ok_or_else(|| anyhow::anyhow!("Relay control channel closed"))?;
// after (caller side)
loop {
    if let Err(e) = start_relay(...).await {
        tracing::warn!(?e, "relay client ended; reconnecting in 2s");
        tokio::time::sleep(Duration::from_secs(2)).await;
    }
}
Defensive patterns

Strategy: retry

Validate before calling

// before connecting, confirm the relay is reachable
let reachable = tokio::net::TcpStream::connect(relay_addr).await.is_ok();
if !reachable {
    return Err(anyhow!("relay {} unreachable; not starting client", relay_addr));
}

Try / catch

if let Err(e) = start_relay(...).await {
    if e.to_string().contains("Relay control channel closed") {
        tracing::warn!(?e, "relay closed; reconnecting with backoff");
        backoff_retry(|| start_relay(...)).await?;
    } else {
        return Err(e);
    }
}

Prevention

When it happens

Trigger: The relay server closes the control connection (restart, idle timeout, crash), the WebSocket/underlying transport to the relay drops, or yamux tears down the session after a protocol error.

Common situations: Relay server redeployed or restarted while clients were connected; network interruption (NAT timeout, VPN drop); client stayed idle past a server-side keepalive limit; mismatched protocol versions causing the server to hang up.

Related errors


AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29). Data as JSON: /api/errors/089ab950eca48809. Report an issue: GitHub.