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

listing commands failed ({})

Error message

listing commands failed ({})

What it means

During slash-command reconcile, listing the application's existing commands (global or per-guild endpoint) returned a non-success status, so the diff against the desired set cannot be computed. A 429 is special-cased earlier into a RateLimited outcome with a persisted cooldown; anything else bails here.

Source

Thrown at crates/zeroclaw-channels/src/discord/slash.rs:636

    // so the fingerprint is not recorded and the next READY retries.
    let mut failed_deletes = 0usize;
    // `with_localizations=true` so the listing echoes back the full
    // `*_localizations` dictionaries; without it Discord returns them null and
    // every localized command would mismatch the projection and re-register on
    // each READY (burning the daily command-create budget).
    let resp = client
        .get(format!("{base}?with_localizations=true"))
        .header("Authorization", auth)
        .send()
        .await
        .map_err(reqwest::Error::without_url)?;
    if resp.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
        return Ok(ReconcileOutcome::RateLimited {
            until: rate_limit_deadline(resp).await,
        });
    }
    if !resp.status().is_success() {
        anyhow::bail!("listing commands failed ({})", resp.status());
    }
    let existing: Vec<serde_json::Value> =
        resp.json().await.map_err(reqwest::Error::without_url)?;
    for cmd in &existing {
        let name = cmd.get("name").and_then(|n| n.as_str()).unwrap_or("");
        if name == "ask" || desired_names.contains(name) || !is_skill_command_shape(cmd) {
            continue;
        }
        let Some(id) = cmd.get("id").and_then(|i| i.as_str()) else {
            continue;
        };
        let del = client
            .delete(format!("{base}/{id}"))
            .header("Authorization", auth)
            .send()
            .await
            .map_err(reqwest::Error::without_url)?;
        if del.status().is_success() || del.status() == reqwest::StatusCode::NOT_FOUND {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. 401 → fix the bot token in channel config
  2. 403 → verify the application id matches the token's application (both must come from the same Discord app)
  3. 5xx → wait for the next gateway READY; reconcile is retried automatically
  4. In guild scope, check that guild_ids are valid snowflakes the bot can see
Defensive patterns

Strategy: retry

Try / catch

match reconcile_slash_commands(&client, &token, &app_id, &desired, base, scope, &guilds).await {
    Ok(ReconcileOutcome::RateLimited { until }) => {
        // cooldown persisted; wait until `until` before the next attempt
        tokio::time::sleep_until(until).await;
    }
    Ok(ReconcileOutcome::Reconciled) => {}
    Err(e) if e.to_string().contains("listing commands failed") => {
        // listing failures are credential-level (401/403) or transient (5xx):
        // check credentials, then let the next READY retry
        tracing::warn!("slash reconcile listing failed: {e}");
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: GET /applications/{app_id}/commands (or /guilds/{guild_id}/commands) answers 401 (invalid bot token), 403 (token does not match the application id), or 5xx — after the 429 branch has already been checked.

Common situations: Application id and bot token from different Discord apps after regenerating credentials; token revoked; Discord 5xx during the READY-triggered reconcile; invalid guild id snowflakes in guild scope.

Related errors


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