tinyhumansai/openhuman · error · anyhow::Error

Composio v3 API error: {err}

Error message

Composio v3 API error: {err}

What it means

Non-2xx from the v3 GET /tools listing (list_actions_v3). The request pins limit=200 and toolkit_versions=latest (without that pin Composio serves the 00000000_00 snapshot and lists zero tools for post-launch toolkits), and adds toolkits/toolkit_slug filters when an app name is given. response_error(resp) folds the HTTP status and response body into {err}, which this bail re-raises.

Source

Thrown at src/openhuman/integrations/composio/tools/direct.rs:231

        }
    }

    async fn list_actions_v3(&self, app_name: Option<&str>) -> anyhow::Result<Vec<ComposioAction>> {
        let url = format!("{}/tools", self.base_v3);
        let mut req = self.client().get(&url).header("x-api-key", &self.api_key);

        // #3932: pin toolkit_versions=latest. Composio v3 otherwise defaults to
        // the 00000000_00 snapshot, which lists zero tools for any toolkit
        // published after it (Outlook and every other post-launch toolkit).
        req = req.query(&[("limit", "200"), ("toolkit_versions", "latest")]);
        if let Some(app) = app_name.map(str::trim).filter(|app| !app.is_empty()) {
            req = req.query(&[("toolkits", app), ("toolkit_slug", app)]);
        }

        let resp = req.send().await?;
        if !resp.status().is_success() {
            let err = response_error(resp).await;
            anyhow::bail!("Composio v3 API error: {err}");
        }

        let body: ComposioToolsResponse = resp
            .json()
            .await
            .context("Failed to decode Composio v3 tools response")?;
        Ok(map_v3_tools_to_actions(body.items))
    }

    async fn list_actions_v2(&self, app_name: Option<&str>) -> anyhow::Result<Vec<ComposioAction>> {
        let mut url = format!("{}/actions", self.base_v2);
        if let Some(app) = app_name {
            url = format!("{url}?appNames={app}");
        }

        let resp = self
            .client()
            .get(&url)

View on GitHub (pinned to 7491200858)

Solutions

  1. Inspect {err} — it contains the HTTP status and Composio's response body
  2. 401: fix the API key (Connections > Composio)
  3. 429: back off and retry; the client does not throttle for you
  4. 400: correct the app/toolkit slug spelling used for toolkits/toolkit_slug
  5. 5xx: transient — retry with backoff and check Composio status
Defensive patterns

Strategy: try-catch

Try / catch

match tool.list_actions(Some(app)).await {
    Ok(actions) => { /* ... */ }
    Err(e) => {
        let msg = format!("{e:#}");
        match () {
            _ if msg.contains("401") => ui.prompt_reenter_composio_key(),
            _ if msg.contains("429") || msg.contains("50") => scheduler.retry_with_backoff(),
            _ => tracing::warn!("[composio] tools listing failed: {msg}"),
        }
    }
}

Prevention

When it happens

Trigger: 401 invalid API key; 400 for a malformed/unknown toolkit slug in the toolkits or toolkit_slug query param; 429 rate limit; 5xx from Composio. Surfaced directly when list_actions_v3 is the failing path, or embedded inside the combined v3+v2 error when the fallback also fails.

Common situations: Bad key or typo'd app name ('gh' vs 'github'); polling too aggressively and hitting rate limits during a Composio incident; a toolkit slug renamed upstream.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/72c7231a60fab457. Report an issue: GitHub.