tinyhumansai/openhuman · error
API request failed: {message}
Error message
API request failed: {message} What it means
parse_api_response_value() unwraps the TinyHumans backend envelope: when the JSON body has success === false, it takes message (falling back to error, then 'request unsuccessful') and bails with 'API request failed: <message>'. This is a business-level failure delivered over HTTP 200 by the backend, after transport and auth already succeeded.
Source
Thrown at src/api/rest.rs:311
/// than `{success,data}`; SDK transport must not expose that envelope detail to
/// existing callers.
fn parse_api_response_value(value: Value) -> Result<Value> {
let Some(object) = value.as_object() else {
return Ok(value);
};
if let Some(user) = object.get("user").filter(|user| !user.is_null()) {
return Ok(user.clone());
}
let Some(success) = object.get("success").and_then(Value::as_bool) else {
return Ok(value);
};
if !success {
let message = object
.get("message")
.or_else(|| object.get("error"))
.and_then(Value::as_str)
.unwrap_or("request unsuccessful");
anyhow::bail!("API request failed: {message}");
}
if let Some(data) = object.get("data").filter(|data| !data.is_null()) {
return Ok(data.clone());
}
if let Some(user) = object.get("user").filter(|user| !user.is_null()) {
return Ok(user.clone());
}
let mut unwrapped = object.clone();
unwrapped.remove("success");
Ok(Value::Object(unwrapped))
}
fn user_id_from_object(obj: &serde_json::Map<String, Value>) -> Option<String> {
for key in ["id", "_id", "userId"] {
if let Some(s) = obj.get(key).and_then(|x| x.as_str()) {
let t = s.trim();
if !t.is_empty() {
return Some(t.to_string());View on GitHub (pinned to a221052e0d)
Solutions
- Read the appended backend message — it is authoritative for the failing rule and names the field/limit involved
- If the message mentions auth/session, re-auth and retry once with a fresh token
- If a previously working call started failing, check for a recent backend deploy and compare the request shape against the SDK's route definition
- Capture route + redacted params for a backend issue if the message is inconclusive
Example fix
// before
let value = client.authed_json(route, body).await?; // bubbles 'API request failed: <backend message>'
// after — branch on session-ish messages and retry once with a refreshed token
match client.authed_json(route, body).await {
Ok(v) => Ok(v),
Err(e) if e.to_string().contains("API request failed")
&& e.to_string().to_lowercase().contains("session") => {
refresh_session().await?;
client.authed_json(route, body).await
}
Err(e) => Err(e),
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-flight nothing meaningful exists: the failure is server-side business logic; // only the request shape can be validated client-side debug_assert!(serde_json::to_value(&body).is_ok(), "body must serialize");
Type guard
fn backend_failure_message(v: &serde_json::Value) -> Option<&str> {
let obj = v.as_object()?;
if obj.get("success").and_then(serde_json::Value::as_bool) == Some(false) {
obj.get("message").or_else(|| obj.get("error")).and_then(serde_json::Value::as_str)
} else { None }
} Try / catch
match client.authed_json(route, body).await {
Ok(v) => Ok(v),
Err(e) if e.to_string().contains("API request failed") => {
let msg = e.to_string();
if msg.to_lowercase().contains("session") || msg.to_lowercase().contains("expired") {
refresh_session().await?; // one retry with a fresh token
client.authed_json(route, body).await
} else {
Err(anyhow::anyhow!("backend rejected {route}: {msg}")) // fatal, surfaced to user
}
}
Err(e) => Err(e),
} Prevention
- classify envelope messages into retryable (session/quota-transient) vs fatal at the call boundary
- keep classify_sdk_error/rest_tests parity when migrating routes (repo convention)
- log route + status context around envelope failures, never payloads with PII
When it happens
Trigger: Any backend endpoint answering {success:false,...}: invalid or failed validation on params; quota/plan limits; account or entitlement rejections on routes not already classified as 401/SESSION_EXPIRED by the caller's error classification.
Common situations: A backend deploy tightened validation so a previously accepted payload now fails; org hitting plan limits; stale session on a route whose 401 classification lives elsewhere; calling a route with fields from an older SDK shape.
Related errors
- Failed to send magic link (${response.status})
- RPC envelope contains undefined data
- threads_token_usage returned an empty envelope
- HTTP error! status: ${response.status}
- Core RPC returned an error
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/c727a2712fa664df.
Report an issue: GitHub.