{"record":{"id":"c727a2712fa664df","repo":"tinyhumansai/openhuman","slug":"api-request-failed-message","errorCode":null,"errorMessage":"API request failed: {message}","messagePattern":"API request failed: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/api/rest.rs","lineNumber":311,"sourceCode":"/// than `{success,data}`; SDK transport must not expose that envelope detail to\n/// existing callers.\nfn parse_api_response_value(value: Value) -> Result<Value> {\n    let Some(object) = value.as_object() else {\n        return Ok(value);\n    };\n    if let Some(user) = object.get(\"user\").filter(|user| !user.is_null()) {\n        return Ok(user.clone());\n    }\n    let Some(success) = object.get(\"success\").and_then(Value::as_bool) else {\n        return Ok(value);\n    };\n    if !success {\n        let message = object\n            .get(\"message\")\n            .or_else(|| object.get(\"error\"))\n            .and_then(Value::as_str)\n            .unwrap_or(\"request unsuccessful\");\n        anyhow::bail!(\"API request failed: {message}\");\n    }\n    if let Some(data) = object.get(\"data\").filter(|data| !data.is_null()) {\n        return Ok(data.clone());\n    }\n    if let Some(user) = object.get(\"user\").filter(|user| !user.is_null()) {\n        return Ok(user.clone());\n    }\n    let mut unwrapped = object.clone();\n    unwrapped.remove(\"success\");\n    Ok(Value::Object(unwrapped))\n}\n\nfn user_id_from_object(obj: &serde_json::Map<String, Value>) -> Option<String> {\n    for key in [\"id\", \"_id\", \"userId\"] {\n        if let Some(s) = obj.get(key).and_then(|x| x.as_str()) {\n            let t = s.trim();\n            if !t.is_empty() {\n                return Some(t.to_string());","sourceCodeStart":293,"sourceCodeEnd":329,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/src/api/rest.rs#L293-L329","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n let value = client.authed_json(route, body).await?;  // bubbles 'API request failed: <backend message>'\n\n// after — branch on session-ish messages and retry once with a refreshed token\n match client.authed_json(route, body).await {\n     Ok(v) => Ok(v),\n     Err(e) if e.to_string().contains(\"API request failed\")\n         && e.to_string().to_lowercase().contains(\"session\") => {\n         refresh_session().await?;\n         client.authed_json(route, body).await\n     }\n     Err(e) => Err(e),\n }","handlingStrategy":"try-catch","validationCode":"// pre-flight nothing meaningful exists: the failure is server-side business logic;\n// only the request shape can be validated client-side\ndebug_assert!(serde_json::to_value(&body).is_ok(), \"body must serialize\");","typeGuard":"fn backend_failure_message(v: &serde_json::Value) -> Option<&str> {\n    let obj = v.as_object()?;\n    if obj.get(\"success\").and_then(serde_json::Value::as_bool) == Some(false) {\n        obj.get(\"message\").or_else(|| obj.get(\"error\")).and_then(serde_json::Value::as_str)\n    } else { None }\n}","tryCatchPattern":"match client.authed_json(route, body).await {\n    Ok(v) => Ok(v),\n    Err(e) if e.to_string().contains(\"API request failed\") => {\n        let msg = e.to_string();\n        if msg.to_lowercase().contains(\"session\") || msg.to_lowercase().contains(\"expired\") {\n            refresh_session().await?;                       // one retry with a fresh token\n            client.authed_json(route, body).await\n        } else {\n            Err(anyhow::anyhow!(\"backend rejected {route}: {msg}\"))  // fatal, surfaced to user\n        }\n    }\n    Err(e) => Err(e),\n}","preventionTips":["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"],"tags":["api","backend","envelope","error-handling"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}