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

  1. Have the user send a new inbound message — the listen loop stores a fresh sessionWebhook — then retry the reply
  2. Reply promptly within the session lifecycle instead of hours later
  3. Verify message.recipient equals the reply_target the channel produced (conversation id for groups, senderStaffId for private chats)
  4. 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

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


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