tinyhumansai/openhuman · error · anyhow::Error
Backend error for {} {}: {}
Error message
Backend error for {} {}: {} What it means
Thrown when a backend response parses as the standard {success, data, error} envelope but success is false. IntegrationClient relays the backend's error string (or "unknown backend error" when the error field is absent) after routing it through the observability classifier with failure=envelope_error, which demotes known user-state errors (toolkit not enabled, missing fields) to breadcrumbs while genuine backend bugs still surface as Sentry events.
Source
Thrown at src/openhuman/integrations/client.rs:492
fn parse_envelope<T: serde::de::DeserializeOwned>(
method: &str,
path: &str,
url: &str,
value: serde_json::Value,
) -> anyhow::Result<T> {
let method_upper = method.to_uppercase();
let envelope: BackendResponse<T> = serde_json::from_value(value)?;
if !envelope.success {
let msg = envelope
.error
.unwrap_or_else(|| "unknown backend error".into());
crate::core::observability::report_error_or_expected(
msg.as_str(),
"integrations",
method,
&[("path", path), ("failure", "envelope_error")],
);
anyhow::bail!("Backend error for {} {}: {}", method_upper, url, msg);
}
envelope.data.ok_or_else(|| {
anyhow::anyhow!(
"Backend returned success but no data for {} {}",
method_upper,
url
)
})
}
async fn request_json<T: serde::de::DeserializeOwned>(
&self,
method: reqwest::Method,
path: &str,
body: Option<&serde_json::Value>,
) -> anyhow::Result<T> {
reject_backend_webhook_path(method.as_str(), path)?;
enforce_backend_egress(path)?;View on GitHub (pinned to 7491200858)
Solutions
- Read the trailing {msg} — it is the backend's own error string and names the actual cause
- If the message indicates auth failure, re-authenticate / refresh the session and retry once
- Verify the integration/toolkit is enabled and authorized for the account before calling its endpoints
- If the message is "unknown backend error", inspect the raw response body — the envelope lacked an error field, which can indicate a backend contract change
Defensive patterns
Strategy: try-catch
Try / catch
match client.post::<T>(path, &body).await {
Ok(data) => Ok(data),
Err(err) => {
let msg = err.to_string();
if let Some(rest) = msg.strip_prefix("Backend error for ") {
let backend_msg = rest.rsplit(": ").next().unwrap_or(rest);
match backend_msg {
m if m.contains("not enabled") => return enable_toolkit_then_retry().await,
m if m.contains("unauthorized") || m.contains("session") => return reauth_and_retry().await,
_ => {}
}
}
Err(err)
}
} Prevention
- Authorize/enable the integration and verify session freshness before issuing integration calls
- Log the full backend message with the path — the suffix is the only actionable part of the error
- Treat 'unknown backend error' as a contract-drift signal and capture the raw body in debug logs
When it happens
Trigger: Any IntegrationClient.post/get where the backend answers {success:false, error:...} — e.g. calling a Composio endpoint for a toolkit that was never authorized, a missing required field, an expired session token, or an upstream provider failure relayed by the backend.
Common situations: Integration used before its toolkit was enabled in the UI; session token expired between calls; backend validation tightened after a version bump; envelope changed shape so the error field is missing (message then reads "unknown backend error").
Related errors
- Backend error for DELETE {}: {}
- API request failed: {message}
- Backend returned {status} for DELETE {url}: {detail}
- manifest fetch failed (${res.status})
- failed to fetch riv (${res.status}) from ${url}
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/543c9c9f7bdb0bb9.
Report an issue: GitHub.