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
- Restart/reconnect: call start_relay again to establish a fresh control channel (implement automatic reconnection with backoff in the caller).
- Check the relay server is running and reachable; inspect server logs for why it closed the session.
- Ensure keepalive/heartbeat settings on the yamux session and underlying transport are configured to survive idle periods.
- 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
- Implement automatic reconnect with exponential backoff around start_relay.
- Enable yamux/transport keepalives to detect dead connections early.
- Monitor relay server restarts and version deploys.
- Surface relay connection state in the UI for faster diagnosis.
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
- Relay yamux session error: {e}
- relay session error: {error}
- Host returned HTTP ${response.status}
- Auth methods lookup failed (${res.status})
- Session refresh failed. Please sign in again.
AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29).
Data as JSON: /api/errors/089ab950eca48809.
Report an issue: GitHub.