zeroclaw-labs/zeroclaw · error · anyhow::Error
webhook reply failed ({status}): {err}
Error message
webhook reply failed ({status}): {err} What it means
DingTalkChannel::send replies by POSTing a markdown message to the per-chat sessionWebhook URL captured from the last inbound message (session_webhooks is keyed by both chat id and sender id). A non-2xx from that webhook becomes this error. DingTalk issues a fresh, short-lived webhook per inbound message, so expired sessions are the dominant cause; a chat with no stored session at all produces a different error ("No session webhook found ...").
Source
Thrown at crates/zeroclaw-channels/src/dingtalk.rs:194
let body = serde_json::json!({
"msgtype": "markdown",
"markdown": {
"title": title,
"text": message.content,
}
});
let resp = self
.http_client()
.post(webhook_url)
.json(&body)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let err = resp.text().await.unwrap_or_default();
anyhow::bail!("webhook reply failed ({status}): {err}");
}
Ok(())
}
async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {
::zeroclaw_log::record!(
INFO,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
"registering gateway connection..."
);
let gw = self.register_connection().await?;
let ws_url = format!("{}?ticket={}", gw.endpoint, gw.ticket);
::zeroclaw_log::record!(
INFO,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),View on GitHub (pinned to 88bb9c8533)
Solutions
- Have the user send a new inbound message — the listen loop stores a fresh sessionWebhook — then retry the reply
- Reply promptly within the session lifecycle instead of hours later
- Verify message.recipient equals the reply_target the channel produced (conversation id for groups, senderStaffId for private chats)
- Inspect the returned DingTalk body for the exact errorCode before assuming expiry
Defensive patterns
Strategy: fallback
Try / catch
match dingtalk.send(&msg).await {
Err(e) if e.to_string().contains("webhook reply failed") => {
// session webhook stale — queue the reply until the next inbound message re-establishes it
}
r => r?,
} Prevention
- Send replies within the same session lifecycle as the inbound message
- Remember session_webhooks is in-memory: after a restart every chat needs a fresh inbound message
- Never build cron-push flows on DingTalk session webhooks — they are per-conversation and short-lived
When it happens
Trigger: Replying long after the last inbound message, when the sessionWebhook has expired; group chat where the recipient key resolves to a different chat's stored URL; markdown payload rejected (empty text/title under msgtype markdown); webhook already consumed or rate-limited by an earlier reply.
Common situations: Scheduled/cron pushes to a chat that has not messaged recently; bot restarted (session_webhooks is in-memory) so old reply targets are gone; agent turns so long the reply lands after webhook expiry.
Related errors
- gateway registration failed ({status}): {err}
- WebSocket stream ended
- interaction followup edit failed ({status}): {err}
- Cannot persist empty LINE userId
- matrix: {reason} Cannot auto-recover because channels.matrix
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/a6f3f35cc66d94c1.
Report an issue: GitHub.