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

GET /channels/{id} returned {}: explicit channel_id is not a

Error message

GET /channels/{id} returned {}: explicit channel_id is not accessible to this bot

What it means

When `channel_ids` lists explicit IDs, `list_target_channels` resolves each one via `GET /api/v4/channels/{id}` with the bot's bearer token. This error means one lookup returned a non-2xx status: the bot token is not valid (401), the bot is not a member of that (private) channel (403), or the ID does not exist / was deleted (404).

Source

Thrown at crates/zeroclaw-channels/src/mattermost.rs:212

            .filter(|id| seen.insert(id.clone()))
            .collect();
        if ids.is_empty() { None } else { Some(ids) }
    }

    pub(crate) async fn list_target_channels(&self) -> Result<Vec<TargetChannel>> {
        let token = self.token().await?.to_string();
        if let Some(ids) = self.scoped_channel_ids() {
            let mut out = Vec::with_capacity(ids.len());
            for id in ids {
                let resp = self
                    .http_client()
                    .get(format!("{}/api/v4/channels/{}", self.base_url, id))
                    .bearer_auth(&token)
                    .send()
                    .await
                    .with_context(|| format!("GET /channels/{id} failed"))?;
                if !resp.status().is_success() {
                    bail!(
                        "GET /channels/{id} returned {}: explicit channel_id is not accessible to this bot",
                        resp.status()
                    );
                }
                let body: serde_json::Value = resp
                    .json()
                    .await
                    .with_context(|| format!("decode /channels/{id} body"))?;
                let ty = body.get("type").and_then(|v| v.as_str()).unwrap_or("");
                out.push(TargetChannel {
                    id,
                    is_direct: is_direct_channel(ty),
                });
            }
            return Ok(out);
        }
        let resp = self
            .http_client()

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Verify the failing ID: `curl -H "Authorization: Bearer <token>" <server>/api/v4/channels/<id>` and check the status
  2. Add the bot to the channel (UI "Add members" or `POST /api/v4/channels/{id}/members` with the bot's user ID)
  3. If status is 401, regenerate/fix the bot token first
  4. Replace the explicit ID with `*` (or drop `channel_ids`) to fall back to auto-discovery of channels the bot already sees

Example fix

# before
[channels.mattermost.team]
channel_ids = ["abc123notjoined"]

# after
[channels.mattermost.team]
channel_ids = ["*"]  # auto-discover, or list only channels the bot has joined
Defensive patterns

Strategy: try-catch

Validate before calling

for id in &channel_ids {
    let resp = client
        .get(format!("{base_url}/api/v4/channels/{id}"))
        .bearer_auth(&token)
        .send()
        .await?;
    if !resp.status().is_success() {
        eprintln!("channel {id} not accessible: {}", resp.status());
    }
}

Try / catch

match mm_channel.listen(tx).await {
    Err(e) if e.to_string().contains("explicit channel_id is not accessible") => {
        // surface which channel_ids entry failed, fix membership or switch to "*"
    }
    other => other,
}

Prevention

When it happens

Trigger: `channels.mattermost.<alias>.channel_ids` contains an inaccessible channel (bot never added, removed, archived, or deleted channel, or a typo'd ID), raised on the first `listen()`, `listen_polling()`, `listen_websocket()`, or direct `list_target_channels()` call.

Common situations: Bot was removed from a private channel after config was written; channel ID copied from a different Mattermost instance; bot account exists but was never invited to the team/channel; personal access token revoked.

Related errors


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