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

Reply API failed ({status}): {err}

Error message

Reply API failed ({status}): {err}

What it means

LINE channel: send_reply POSTs to /v2/bot/message/reply with the webhook's replyToken and got non-2xx. LINE reply tokens are single-use and short-lived, so the dominant cause is an expired or already-consumed token — typically because the agent's processing (e.g. LLM latency) exceeded the token's validity, or a reply was already sent for that webhook. Status and error body are included.

Source

Thrown at crates/zeroclaw-channels/src/line.rs:1044

        // LINE Reply API accepts at most 5 messages per call.
        for batch in messages.chunks(5) {
            let body = serde_json::json!({
                "replyToken": reply_token,
                "messages": batch,
            });
            let resp = self
                .client
                .post(&url)
                .bearer_auth(&self.channel_access_token)
                .json(&body)
                .send()
                .await?;

            if !resp.status().is_success() {
                let status = resp.status();
                let err = resp.text().await.unwrap_or_default();
                anyhow::bail!("Reply API failed ({status}): {err}");
            }
        }
        Ok(())
    }

    /// Send text via the Push API (requires a paid LINE plan for high volume).
    async fn send_push(&self, to: &str, text: &str) -> anyhow::Result<()> {
        let url = format!("{}/v2/bot/message/push", self.api_base_url);
        let sender_name = (self.sender_name_resolver)()
            .filter(|s| !s.is_empty())
            .unwrap_or_else(|| "AI".to_string());
        let sender_icon = self.sender_icon.read().clone();
        let messages: Vec<serde_json::Value> = Self::split_message(text)
            .into_iter()
            .map(|chunk| {
                let mut msg = serde_json::json!({"type": "text", "text": chunk});
                if let Some(sender) = Self::build_sender_obj(&sender_name, &sender_icon) {
                    msg["sender"] = sender;

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Cut time-to-first-reply: reply immediately (e.g. a 'thinking...' message) or reduce processing latency, then follow up via push.
  2. Never reuse a replyToken — send exactly one reply per webhook event and drop the token afterwards.
  3. If the token is already expired/used, fall back to the Push API for that recipient.
  4. 401 -> fix the channel access token (see error 116).

Example fix

// before: hold the reply token through slow inference, then reply once
let reply = agent.generate(input).await?;
line.send_reply(&reply_token, &reply).await?;

// after: fail over to push when the reply token is spent/expired
if let Err(e) = line.send_reply(&reply_token, &reply).await {
    tracing::warn!(error = %e, "reply token unusable; falling back to push");
    line.send_push(&user_id, &reply).await?;
}
Defensive patterns

Strategy: fallback

Validate before calling

// Track reply token freshness: drop tokens older than a few seconds and reply fast
struct ReplyToken { token: String, issued_at: std::time::Instant }
impl ReplyToken {
    fn usable(&self) -> bool { self.issued_at.elapsed() < Duration::from_secs(50) && !self.consumed }
}

Type guard

fn is_reply_token_spent(err: &anyhow::Error) -> bool {
    let s = err.to_string();
    s.contains("Reply API failed") && (s.contains("400") || s.contains("Invalid reply token"))
}

Try / catch

if let Err(e) = line.send_reply(&reply_token, &msg).await {
    if is_reply_token_spent(&e) {
        tracing::warn!("reply token expired/used; falling back to push");
        return line.send_push(&user_id, &msg).await;
    }
    return Err(e);
}

Prevention

When it happens

Trigger: Reply token expired due to slow processing between webhook receipt and reply; replyToken already used by an earlier reply or a duplicate webhook delivery; 401 with an invalid channel access token; malformed reply payload (400).

Common situations: High-latency agent pipelines that hold the reply token during long inference; retry logic re-sending with the same token; fan-out handlers where two paths both reply to one webhook.

Related errors


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