tinyhumansai/openhuman · error · anyhow::Error
composio.authorize: extra_params cannot override reserved ke
Error message
composio.authorize: extra_params cannot override reserved key '{k}' What it means
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.
Source
Thrown at src/openhuman/integrations/composio/client.rs:104
extra_params: Option<serde_json::Value>,
) -> Result<ComposioAuthorizeResponse> {
let toolkit = toolkit.trim();
if toolkit.is_empty() {
anyhow::bail!("composio.authorize: toolkit must not be empty");
}
tracing::debug!(toolkit = %toolkit, has_extra_params = extra_params.is_some(), "[composio] authorize");
let mut body = serde_json::json!({ "toolkit": toolkit });
if let Some(extra) = extra_params {
const RESERVED: &[&str] = &["toolkit", "toolkit_version", "auth", "client_id"];
let extra_obj = extra.as_object().ok_or_else(|| {
anyhow::anyhow!("composio.authorize: extra_params must be a JSON object")
})?;
let obj = body.as_object_mut().ok_or_else(|| {
anyhow::anyhow!("composio.authorize: internal payload must be an object")
})?;
for (k, v) in extra_obj {
if RESERVED.contains(&k.as_str()) {
anyhow::bail!(
"composio.authorize: extra_params cannot override reserved key '{k}'"
);
}
obj.insert(k.clone(), v.clone());
}
}
merge_required_oauth_scopes(&mut body, toolkit)?;
self.inner
.post::<ComposioAuthorizeResponse>("/agent-integrations/composio/authorize", &body)
.await
}
/// `DELETE /agent-integrations/composio/connections/{id}`.
///
/// The backend verifies that the caller owns the connection before
/// deleting it. We call this via `POST` with a synthetic `_method`
/// body because [`IntegrationClient`] does not currently expose a
/// generic `delete()` — the backend accepts the method override.View on GitHub (pinned to 7491200858)
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
Example fix
// before
let extra = serde_json::json!({ "waba_id": waba, "client_id": "123" });
client.authorize("whatsapp", Some(extra)).await?;
// after — drop reserved keys at the call boundary
const RESERVED: &[&str] = &["toolkit", "toolkit_version", "auth", "client_id"];
let extra: serde_json::Map<String, serde_json::Value> = extra
.as_object()
.cloned()
.unwrap_or_default()
.into_iter()
.filter(|(k, _)| !RESERVED.contains(&k.as_str()))
.collect();
client.authorize("whatsapp", Some(serde_json::Value::Object(extra))).await?; Defensive patterns
Strategy: validation
Validate before calling
const RESERVED: &[&str] = &["toolkit", "toolkit_version", "auth", "client_id"];
fn sanitize_extra_params(extra: &serde_json::Value) -> serde_json::Value {
let filtered: serde_json::Map<String, serde_json::Value> = extra
.as_object()
.cloned()
.unwrap_or_default()
.into_iter()
.filter(|(k, _)| !RESERVED.contains(&k.as_str()))
.collect();
serde_json::Value::Object(filtered)
} Type guard
fn extra_params_safe(extra: &serde_json::Value) -> bool {
const RESERVED: &[&str] = &["toolkit", "toolkit_version", "auth", "client_id"];
extra
.as_object()
.map(|o| o.keys().all(|k| !RESERVED.contains(&k.as_str())))
.unwrap_or(false)
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- composio.authorize: toolkit must not be empty
- composio direct authorize: toolkit must not be empty
- composio.execute_tool: tool slug must not be empty
- composio.delete_connection: connectionId must not be empty
- composio.execute_tool: tool slug must not be empty
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/0b2d483d827147b8.
Report an issue: GitHub.