{"record":{"id":"e9541d31b0810516","repo":"tinyhumansai/openhuman","slug":"threadid-is-required","errorCode":null,"errorMessage":"threadId is required","messagePattern":"threadId is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/api/rest.rs","lineNumber":1080,"sourceCode":"            bearer_jwt,\n            Method::POST,\n            &format!(\"channels/{encoded}/threads\"),\n            Some(body),\n        )\n        .await\n    }\n\n    /// Updates an existing thread (e.g., closing or reopening it).\n    pub async fn update_channel_thread(\n        &self,\n        channel: &str,\n        bearer_jwt: &str,\n        thread_id: &str,\n        action: &str,\n    ) -> Result<Value> {\n        let channel = channel.trim().trim_matches('/');\n        anyhow::ensure!(!channel.is_empty(), \"channel is required\");\n        anyhow::ensure!(!thread_id.trim().is_empty(), \"threadId is required\");\n        anyhow::ensure!(\n            action == \"close\" || action == \"reopen\",\n            \"action must be 'close' or 'reopen'\"\n        );\n        let encoded_channel = urlencoding::encode(channel);\n        let encoded_thread = urlencoding::encode(thread_id.trim());\n        let body = serde_json::json!({ \"action\": action });\n        self.authed_json(\n            bearer_jwt,\n            Method::PATCH,\n            &format!(\"channels/{encoded_channel}/threads/{encoded_thread}\"),\n            Some(body),\n        )\n        .await\n    }\n\n    /// Lists threads in a communication channel, optionally filtering by status.\n    pub async fn list_channel_threads(","sourceCodeStart":1062,"sourceCodeEnd":1098,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/src/api/rest.rs#L1062-L1098","documentation":"Thrown by update_channel_thread when thread_id is empty after trimming (the channel and action checks are siblings in the same function). The thread id is URL-encoded into PATCH channels/{channel}/threads/{thread_id} — an empty segment would address the collection, not a thread. Input validation only, no request is sent.","triggerScenarios":"Calling update_channel_thread with thread_id = \"\" or \"   \" — e.g. the close/reopen action fired on a locally-created thread before the backend thread id was stored.","commonSituations":"UI optimistically creates a thread row with an empty pending id, or the thread_id field was renamed and now reads a missing key.","solutions":["Only enable close/reopen once the backend thread id is persisted","Validate thread_id non-empty at the action boundary","Guard at the call site with trim()"],"exampleFix":"// before\nclient.update_channel_thread(&channel, &jwt, &thread_id, \"reopen\").await?;\n\n// after\nlet thread_id = thread_id.trim();\nanyhow::ensure!(!thread_id.is_empty(), \"threadId is required (backend thread not created yet?)\");\nclient.update_channel_thread(&channel, &jwt, thread_id, \"reopen\").await?;","handlingStrategy":"validation","validationCode":"let thread_id = thread_id.trim();\nanyhow::ensure!(!thread_id.is_empty(), \"threadId is required (backend thread not created yet?)\");\nclient.update_channel_thread(&channel, &jwt, thread_id, action).await?;","typeGuard":"fn has_thread_id(s: &str) -> bool {\n    !s.trim().is_empty()\n}","tryCatchPattern":null,"preventionTips":["Only enable close/reopen once the backend thread id is persisted locally","Give pending local threads a typed Pending state instead of an empty-string id"],"tags":["rust","validation","thread","id"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}