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

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

Error message

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

What it means

LINE channel: send_push POSTs to /v2/bot/message/push and got non-2xx. Push bypasses reply tokens but is restricted by plan and quota on the LINE Messaging API, so the frequent causes are plan-level denial or quota exhaustion rather than payload shape. Status and error body are included.

Source

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

            .collect();

        for batch in messages.chunks(5) {
            let body = serde_json::json!({
                "to": to,
                "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!("Push API failed ({status}): {err}");
            }
        }
        Ok(())
    }

    pub(crate) async fn listen_with_listener(
        &self,
        listener: tokio::net::TcpListener,
        bot_user_id: String,
        tx: tokio::sync::mpsc::Sender<ChannelMessage>,
    ) -> anyhow::Result<()> {
        let state = Arc::new(LineState {
            tx,
            channel_secret: self.channel_secret.clone(),
            bot_user_id,
            dm_policy: self.dm_policy.clone(),
            group_policy: self.group_policy.clone(),
            alias: self.alias.clone(),

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Branch on status: 403 -> enable/upgrade the messaging plan in the LINE console; 429 -> respect quota headers and back off or resume next cycle; 400 -> verify the userId format; 401 -> refresh the channel access token.
  2. Monitor remaining quota and alert before exhaustion.
  3. Where possible prefer replies over pushes to conserve quota.
Defensive patterns

Strategy: retry

Validate before calling

// Validate target shape before pushing
fn is_line_user_id(s: &str) -> bool { s.starts_with('U') && s.len() == 33 }
anyhow::ensure!(is_line_user_id(&user_id), "invalid LINE userId for push: {user_id}");

Type guard

fn is_push_quota_or_plan_error(err: &anyhow::Error) -> bool {
    let s = err.to_string();
    s.contains("Push API failed") && (s.contains("403") || s.contains("429"))
}

Try / catch

match line.send_push(&user_id, &msg).await {
    Err(e) if e.to_string().contains("Push API failed (429") => schedule_retry_after_quota_reset(msg),
    Err(e) if e.to_string().contains("Push API failed (403") => alert("plan does not allow push; enable in console"),
    other => other,
}

Prevention

When it happens

Trigger: 403 when the channel's plan does not permit push messages; 429 when the monthly/daily message quota is exhausted; 400 with an invalid userId (must be a LINE user ID starting with 'U', not a room/group id misused); 401 with an invalid channel access token.

Common situations: Trials or downgraded plans losing push access mid-deployment; volume spikes exhausting quota near month-end; using a reply userId where a different target type is required.

Related errors


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