{"record":{"id":"2b30146cc2c4ccb5","repo":"zeroclaw-labs/zeroclaw","slug":"composio-v3-api-error-err","errorCode":null,"errorMessage":"Composio v3 API error: {err}","messagePattern":"Composio v3 API error: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/composio.rs","lineNumber":74,"sourceCode":"    pub async fn list_actions(\n        &self,\n        app_name: Option<&str>,\n    ) -> anyhow::Result<Vec<ComposioAction>> {\n        self.list_actions_v3(app_name).await\n    }\n\n    async fn list_actions_v3(&self, app_name: Option<&str>) -> anyhow::Result<Vec<ComposioAction>> {\n        let url = format!(\"{COMPOSIO_API_BASE_V3}/tools\");\n        let req = self\n            .client()\n            .get(&url)\n            .header(\"x-api-key\", &self.api_key)\n            .query(&Self::build_list_actions_v3_query(app_name));\n\n        let resp = req.send().await?;\n        if !resp.status().is_success() {\n            let err = response_error(resp).await;\n            anyhow::bail!(\"Composio v3 API error: {err}\");\n        }\n\n        let body: ComposioToolsResponse = resp\n            .json()\n            .await\n            .context(\"Failed to decode Composio v3 tools response\")?;\n        self.update_action_slug_cache_from_v3_items(&body.items);\n        Ok(map_v3_tools_to_actions(body.items))\n    }\n\n    fn update_action_slug_cache_from_v3_items(&self, items: &[ComposioV3Tool]) {\n        for item in items {\n            let Some(slug) = item.slug.as_deref().or(item.name.as_deref()) else {\n                continue;\n            };\n            self.cache_action_slug(slug, slug);\n            if let Some(name) = item.name.as_deref() {\n                self.cache_action_slug(name, slug);","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/composio.rs#L56-L92","documentation":"list_actions_v3 sends GET {base}/tools with the x-api-key header and query params limit=200, toolkit_versions=latest, plus toolkits/toolkit_slug filters when an app is given (composio.rs:63-75, 320-335). If the response status is not 2xx, response_error formats it as 'HTTP <code>' plus the API's error message, sanitized (entity/user ids redacted, capped at 240 chars) (composio.rs:1039-1080). This bail wraps that text; it surfaces from tool action='list' and also inside execute_action's slug-cache priming as 'Failed to refresh action list for app ...'.","triggerScenarios":"Calling ComposioTool::list_actions / tool action='list' when: the api_key is invalid/expired (401/403), the 'app' slug does not exist so the toolkits filter is rejected (400), the Composio quota is exhausted (429), or the backend is erroring (5xx). Also fires indirectly when action='execute' cannot resolve a slug and tries to refresh the action list.","commonSituations":"Rotated or mistyped Composio API keys, app slugs guessed by the LLM ('gmail_send' instead of 'gmail'), hitting workspace rate limits during heavy agent loops, and v3 API contract drift when running an older zeroclaw build.","solutions":["Verify the key out-of-band: curl -H 'x-api-key: <key>' 'https://backend.composio.dev/api/v3/tools?limit=1' should return 200; if 401/403, fix the composio api_key in config.","Confirm the app slug against Composio's catalog and pass the bare toolkit name (e.g. 'gmail'), not an action name.","On HTTP 429 or 5xx, retry with backoff (the client already uses 60s connect/10s pool timeouts via build_runtime_proxy_client_with_timeouts).","If errors persist with a valid key, check Composio status/announcements for a v3 API change and update zeroclaw."],"exampleFix":"// before\nlet actions = composio.list_actions(Some(app)).await?;\n\n// after: retry transient failures, surface auth errors immediately\nlet actions = match composio.list_actions(Some(app)).await {\n    Ok(actions) => actions,\n    Err(e) if e.to_string().contains(\"HTTP 429\") || e.to_string().contains(\"HTTP 5\") => {\n        tokio::time::sleep(std::time::Duration::from_secs(2)).await;\n        composio.list_actions(Some(app)).await?\n    }\n    Err(e) => return Err(e),\n};","handlingStrategy":"retry","validationCode":"fn composio_call_ready(api_key: &str, app: Option<&str>) -> Result<(), String> {\n    if api_key.trim().is_empty() {\n        return Err(\"Composio api_key is empty\".into());\n    }\n    if let Some(app) = app {\n        let slug = app.trim().to_ascii_lowercase();\n        if slug.is_empty() || slug.chars().any(|c| c.is_whitespace()) {\n            return Err(format!(\"suspicious app slug: '{app}'\"));\n        }\n    }\n    Ok(())\n}","typeGuard":null,"tryCatchPattern":"let mut attempt = 0;\nloop {\n    attempt += 1;\n    match composio.list_actions(Some(app)).await {\n        Ok(actions) => break actions,\n        Err(e) if attempt < 3 && (e.to_string().contains(\"HTTP 429\") || e.to_string().contains(\"HTTP 5\")) => {\n            tokio::time::sleep(std::time::Duration::from_secs(2u64 * attempt as u64)).await;\n        }\n        Err(e) => {\n            // 401/403/400 are not transient: surface immediately\n            return Err(e);\n        }\n    }\n}","preventionTips":["Fail fast on empty/placeholder api_key at startup instead of at first list call.","Pass verified toolkit slugs (e.g. 'gmail'), never action names, as the app filter.","Retry only HTTP 429/5xx with backoff; treat 401/403 as a config fix, 400 as a slug fix.","Pin a recent zeroclaw build so the v3 endpoint contract matches what Composio serves."],"tags":["composio","http","api-key","rate-limit","rust","zeroclaw"],"backgroundTag":"upstream-api-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}