zeroclaw-labs/zeroclaw · error · anyhow::Error

initial ping failed

Error message

initial ping failed

What it means

Immediately after the WebSocket handshake in listen_ws, the client sends a protobuf 'ping' frame (the same frame the official SDK sends first so the server starts ponging and the ping_interval can be calibrated). If write.send on the split sink errors on that very first frame, the connection is already dead — the server (or an intermediary proxy) closed the stream between handshake completion and the first write — and the loop bails before entering the event loop.

Source

Thrown at crates/zeroclaw-channels/src/lark.rs:1161

        // starts responding with pongs and we can calibrate the ping_interval.
        seq = seq.wrapping_add(1);
        let initial_ping = PbFrame {
            seq_id: seq,
            log_id: 0,
            service: service_id,
            method: 0,
            headers: vec![PbHeader {
                key: "type".into(),
                value: "ping".into(),
            }],
            payload: None,
        };
        if write
            .send(WsMsg::Binary(initial_ping.encode_to_vec().into()))
            .await
            .is_err()
        {
            anyhow::bail!("initial ping failed");
        }
        // message_id → (fragment_slots, created_at) for multi-part reassembly
        type FragEntry = (Vec<Option<Vec<u8>>>, Instant);
        let mut frag_cache: HashMap<String, FragEntry> = HashMap::new();

        loop {
            tokio::select! {
                biased;

                _ = hb_interval.tick() => {
                    seq = seq.wrapping_add(1);
                    let ping = PbFrame {
                        seq_id: seq, log_id: 0, service: service_id, method: 0,
                        headers: vec![PbHeader { key: "type".into(), value: "ping".into() }],
                        payload: None,
                    };
                    if write.send(WsMsg::Binary(ping.encode_to_vec().into())).await.is_err() {
                        ::zeroclaw_log::record!(WARN, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_outcome(::zeroclaw_log::EventOutcome::Unknown), "ping failed, reconnecting");

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Retry the whole listen cycle — a new get_ws_endpoint call provisions a fresh wss URL, which fixes expired-URL cases
  2. If it fails repeatedly, verify proxy settings for channel.lark (ws_connect_with_proxy uses self.proxy_url) — try without the proxy or with a CONNECT-tunneling-capable one
  3. Log the wss URL's service_id (already logged at connect) and confirm it is non-zero — a 0 service_id means the URL querystring was malformed
  4. Check the app's long-connection mode and credentials, since servers sometimes accept the handshake then drop unauthorized clients
Defensive patterns

Strategy: retry

Try / catch

loop {
    if let Err(e) = lark_channel.listen(tx.clone()).await {
        if e.to_string().contains("initial ping failed") || e.to_string().contains("WS endpoint") {
            tokio::time::sleep(backoff.next()).await; // full retry provisions a fresh wss URL
            continue;
        }
        return Err(e);
    }
}

Prevention

When it happens

Trigger: get_ws_endpoint succeeded and ws_connect_with_proxy completed the TLS/WS handshake, but the write half is closed when sending the initial PbFrame ping: server rejected the connection post-handshake (bad service_id parsed from the wss URL), a proxy (self.proxy_url) tore the tunnel down, or the endpoint URL expired between provisioning and connect.

Common situations: Corporate proxy or self.proxy_url misconfiguration killing fresh WS tunnels; reconnecting with a stale wss URL after a network change; clock skew or token expiry invalidating the signed connection.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/2229203d385be5d9. Report an issue: GitHub.