{"record":{"id":"0b2d483d827147b8","repo":"tinyhumansai/openhuman","slug":"composio-authorize-extra-params-cannot-override-r","errorCode":null,"errorMessage":"composio.authorize: extra_params cannot override reserved key '{k}'","messagePattern":"composio\\.authorize: extra_params cannot override reserved key '(.+?)'","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src/openhuman/integrations/composio/client.rs","lineNumber":104,"sourceCode":"        extra_params: Option<serde_json::Value>,\n    ) -> Result<ComposioAuthorizeResponse> {\n        let toolkit = toolkit.trim();\n        if toolkit.is_empty() {\n            anyhow::bail!(\"composio.authorize: toolkit must not be empty\");\n        }\n        tracing::debug!(toolkit = %toolkit, has_extra_params = extra_params.is_some(), \"[composio] authorize\");\n        let mut body = serde_json::json!({ \"toolkit\": toolkit });\n        if let Some(extra) = extra_params {\n            const RESERVED: &[&str] = &[\"toolkit\", \"toolkit_version\", \"auth\", \"client_id\"];\n            let extra_obj = extra.as_object().ok_or_else(|| {\n                anyhow::anyhow!(\"composio.authorize: extra_params must be a JSON object\")\n            })?;\n            let obj = body.as_object_mut().ok_or_else(|| {\n                anyhow::anyhow!(\"composio.authorize: internal payload must be an object\")\n            })?;\n            for (k, v) in extra_obj {\n                if RESERVED.contains(&k.as_str()) {\n                    anyhow::bail!(\n                        \"composio.authorize: extra_params cannot override reserved key '{k}'\"\n                    );\n                }\n                obj.insert(k.clone(), v.clone());\n            }\n        }\n        merge_required_oauth_scopes(&mut body, toolkit)?;\n        self.inner\n            .post::<ComposioAuthorizeResponse>(\"/agent-integrations/composio/authorize\", &body)\n            .await\n    }\n\n    /// `DELETE /agent-integrations/composio/connections/{id}`.\n    ///\n    /// The backend verifies that the caller owns the connection before\n    /// deleting it. We call this via `POST` with a synthetic `_method`\n    /// body because [`IntegrationClient`] does not currently expose a\n    /// generic `delete()` — the backend accepts the method override.","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/integrations/composio/client.rs#L86-L122","documentation":"authorize(toolkit, extra_params) merges the extra_params object into the request body, but refuses any key that collides with the reserved payload fields: toolkit, toolkit_version, auth, client_id. The guard prevents silently overriding the identity/auth fields the client itself constructs — a malformed authorize request that Composio would misinterpret.","triggerScenarios":"Calling authorize with extra_params containing any of the four reserved keys, e.g. {\"client_id\": \"...\"} or {\"auth\": {...}}. Common when forwarding a whole raw provider/auth payload as extra params instead of only the additional fields (like whatsapp's waba_id).","commonSituations":"Copying a full Composio auth payload from docs or a working curl into extra_params; a new key added to the RESERVED list in a version bump now rejecting payloads that used to pass; generic key-value passthrough from a frontend form.","solutions":["Pass only genuinely additional fields in extra_params (e.g. waba_id for whatsapp) and strip toolkit, toolkit_version, auth, client_id before calling","If you meant to change the toolkit, change the toolkit argument — not extra_params","Log the rejected key name from the error message to find which producer sets it"],"exampleFix":"// before\nlet extra = serde_json::json!({ \"waba_id\": waba, \"client_id\": \"123\" });\nclient.authorize(\"whatsapp\", Some(extra)).await?;\n\n// after — drop reserved keys at the call boundary\nconst RESERVED: &[&str] = &[\"toolkit\", \"toolkit_version\", \"auth\", \"client_id\"];\nlet extra: serde_json::Map<String, serde_json::Value> = extra\n    .as_object()\n    .cloned()\n    .unwrap_or_default()\n    .into_iter()\n    .filter(|(k, _)| !RESERVED.contains(&k.as_str()))\n    .collect();\nclient.authorize(\"whatsapp\", Some(serde_json::Value::Object(extra))).await?;","handlingStrategy":"validation","validationCode":"const RESERVED: &[&str] = &[\"toolkit\", \"toolkit_version\", \"auth\", \"client_id\"];\n\nfn sanitize_extra_params(extra: &serde_json::Value) -> serde_json::Value {\n    let filtered: serde_json::Map<String, serde_json::Value> = extra\n        .as_object()\n        .cloned()\n        .unwrap_or_default()\n        .into_iter()\n        .filter(|(k, _)| !RESERVED.contains(&k.as_str()))\n        .collect();\n    serde_json::Value::Object(filtered)\n}","typeGuard":"fn extra_params_safe(extra: &serde_json::Value) -> bool {\n    const RESERVED: &[&str] = &[\"toolkit\", \"toolkit_version\", \"auth\", \"client_id\"];\n    extra\n        .as_object()\n        .map(|o| o.keys().all(|k| !RESERVED.contains(&k.as_str())))\n        .unwrap_or(false)\n}","tryCatchPattern":null,"preventionTips":["Never forward raw provider payloads as extra_params — extract only the additional fields first","Keep the reserved-key list in one place and assert your payload builder excludes it in tests","Document in the calling API that extra_params is additive-only, not an override mechanism"],"tags":["composio","authorize","validation","reserved-keys","payload"],"backgroundTag":"reserved-key-conflict","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}