{"record":{"id":"39b23918fa7fe3c9","repo":"tinyhumansai/openhuman","slug":"composio-action-listing-failed-on-v3-v3-err-an","errorCode":null,"errorMessage":"Composio action listing failed on v3 ({v3_err}) and v2 fallback ({v2_err})","messagePattern":"Composio action listing failed on v3 \\((.+?)\\) and v2 fallback \\((.+?)\\)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src/openhuman/integrations/composio/tools/direct.rs","lineNumber":208,"sourceCode":"            return Ok(());\n        }\n        ensure_https(url)\n    }\n\n    /// List available Composio apps/actions for the authenticated user.\n    ///\n    /// Uses v3 endpoint first and falls back to v2 for compatibility.\n    pub async fn list_actions(\n        &self,\n        app_name: Option<&str>,\n    ) -> anyhow::Result<Vec<ComposioAction>> {\n        match self.list_actions_v3(app_name).await {\n            Ok(items) => Ok(items),\n            Err(v3_err) => {\n                let v2 = self.list_actions_v2(app_name).await;\n                match v2 {\n                    Ok(items) => Ok(items),\n                    Err(v2_err) => anyhow::bail!(\n                        \"Composio action listing failed on v3 ({v3_err}) and v2 fallback ({v2_err})\"\n                    ),\n                }\n            }\n        }\n    }\n\n    async fn list_actions_v3(&self, app_name: Option<&str>) -> anyhow::Result<Vec<ComposioAction>> {\n        let url = format!(\"{}/tools\", self.base_v3);\n        let mut req = self.client().get(&url).header(\"x-api-key\", &self.api_key);\n\n        // #3932: pin toolkit_versions=latest. Composio v3 otherwise defaults to\n        // the 00000000_00 snapshot, which lists zero tools for any toolkit\n        // published after it (Outlook and every other post-launch toolkit).\n        req = req.query(&[(\"limit\", \"200\"), (\"toolkit_versions\", \"latest\")]);\n        if let Some(app) = app_name.map(str::trim).filter(|app| !app.is_empty()) {\n            req = req.query(&[(\"toolkits\", app), (\"toolkit_slug\", app)]);\n        }","sourceCodeStart":190,"sourceCodeEnd":226,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/integrations/composio/tools/direct.rs#L190-L226","documentation":"ComposioTool::list_actions tries GET v3 /tools first (limit=200, toolkit_versions=latest) and falls back to legacy GET v2 /actions; this error means BOTH attempts failed. The message embeds both underlying errors (v3 first), so the pair tells you whether the cause is shared (auth, network, outage) or v3-specific breakage that the v2 fallback could not paper over.","triggerScenarios":"Invalid API key (401 on both endpoints); no network egress to backend.composio.dev; a Composio outage returning 5xx on both API versions; 429 rate limiting on both. Concretely: list_actions(Some(\"github\")) with a revoked key yields 'v3 (Composio v3 API error: 401 ...) and v2 fallback (Composio v2 API error: 401 ...)'.","commonSituations":"Expired/revoked key during app usage; laptop offline or DNS failure; Composio incident; CI environment with blocked egress running integration tests.","solutions":["Read both embedded errors: identical 401s point at the API key, both timing out points at network/egress, differing 4xx bodies point at malformed query params","Verify the key: curl -H 'x-api-key: <key>' 'https://backend.composio.dev/api/v3/tools?limit=1'","For transient 5xx/429, retry with backoff — the built-in v3-to-v2 fallback is already exhausted","If v3 consistently fails while v2 works (or vice versa), capture both messages and check the Composio changelog/status page"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"match tool.list_actions(Some(app)).await {\n    Ok(actions) => Ok(actions),\n    Err(e) => {\n        let msg = format!(\"{e:#}\");\n        if msg.contains(\"401\") {\n            Err(e) // auth is broken on both versions — fix the key, retrying is pointless\n        } else if msg.contains(\"429\") || msg.contains(\"5\") /* transient shapes */ {\n            backoff::retry_after(Duration::from_secs(30), 3).await?; // then re-invoke once\n            Err(e)\n        } else { Err(e) }\n    }\n}","preventionTips":["Log both embedded errors (v3 and v2) — their relationship identifies auth vs network vs v3-specific breakage","Verify the API key works with a one-off curl before wiring an environment","Cache the last successful action list so listing outages degrade gracefully","Add jittered backoff around 429s instead of immediate re-listing"],"tags":["composio","api","fallback-exhausted","actions","listing","network"],"backgroundTag":"upstream-api-fallback-exhausted","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}