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

  1. Read the appended backend message — it is authoritative for the failing rule and names the field/limit involved
  2. If the message mentions auth/session, re-auth and retry once with a fresh token
  3. If a previously working call started failing, check for a recent backend deploy and compare the request shape against the SDK's route definition
  4. 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

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


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/c727a2712fa664df. Report an issue: GitHub.