zeroclaw-labs/zeroclaw · error
WeCom WS {command} ack timeout after {}s (req_id={req_id})
Error message
WeCom WS {command} ack timeout after {}s (req_id={req_id}) What it means
Every WeCom WS command waits at most WECOM_COMMAND_TIMEOUT_SECS = 10 seconds for its ack, matched by req_id; on timeout the pending entry is removed and this error bails with the command name and req_id. The frame was sent but WeCom never answered in time.
Source
Thrown at crates/zeroclaw-channels/src/wecom_ws.rs:415
let (tx, rx) = tokio::sync::oneshot::channel();
self.pending_responses
.lock()
.await
.insert(req_id.to_string(), tx);
if let Err(err) = self.ws_send_frame(frame).await {
self.pending_responses.lock().await.remove(req_id);
return Err(err);
}
match tokio::time::timeout(Duration::from_secs(WECOM_COMMAND_TIMEOUT_SECS), rx).await {
Ok(Ok(result)) => result,
Ok(Err(_)) => anyhow::bail!(
"WeCom WS {command} response channel closed before ack (req_id={req_id})"
),
Err(_) => {
self.pending_responses.lock().await.remove(req_id);
anyhow::bail!(
"WeCom WS {command} ack timeout after {}s (req_id={req_id})",
WECOM_COMMAND_TIMEOUT_SECS
);
}
}
}
async fn maybe_handle_command_response(&self, frame: &Value) -> bool {
let Some(req_id) = frame
.get("headers")
.and_then(|headers| headers.get("req_id"))
.and_then(Value::as_str)
else {
return false;
};
let Some(errcode) = frame.get("errcode").and_then(Value::as_i64) else {
return false;View on GitHub (pinned to 88bb9c8533)
Solutions
- Retry the command once — transient upstream latency is the usual cause
- Reduce markdown chunk size/count so WeCom acks faster
- If timeouts are constant, force a WS reconnect — the socket is likely half-dead — and verify latency to the WeCom endpoint
- Correlate the req_id in logs to confirm whether the ack arrived late vs never
Example fix
// before
let r = channel.send_markdown_chunks(scope, chunks).await?;
// after
let r = retry_on("ack timeout", 2, ||
channel.send_markdown_chunks(scope.clone(), chunks.clone())).await?; Defensive patterns
Strategy: retry
Try / catch
Catch the ack-timeout bail, retry the command once after a short backoff; if the second attempt also times out, force a WS reconnect and try once more before dead-lettering the message.
Prevention
- Keep individual WS commands small so acks come back fast
- Force a reconnect after repeated ack timeouts rather than hammering a half-dead socket
- Track command ack latency to catch degradation before timeouts appear
When it happens
Trigger: ws_send_respond_msg or send_markdown_chunks_to_scope when the WeCom server does not ack within 10s: upstream latency spikes, a half-dead socket that delivers frames but stalls responses, or slow processing of large chunked markdown payloads.
Common situations: Network degradation where the socket stays open but stalls; oversized markdown chunk sequences; WeCom service-side latency events.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- WeCom WS {command} response channel closed before ack (req_i
- Mattermost WebSocket authentication handshake timed out
- WeCom WebSocket channel requires the `channel-wecom-ws` feat
- wecom_ws channel is not connected
- WeCom webhook send failed ({status}): {err}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/44b263175ad98f94.
Report an issue: GitHub.