{"record":{"id":"4639696faa4d30fd","repo":"gitbutlerapp/gitbutler","slug":"github-graphql-addpullrequestreviewthreadreply-returned-no","errorCode":null,"errorMessage":"GitHub GraphQL addPullRequestReviewThreadReply returned no comment","messagePattern":"GitHub GraphQL addPullRequestReviewThreadReply returned no comment","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-github/src/client.rs","lineNumber":1268,"sourceCode":"        #[serde(rename_all = \"camelCase\")]\n        struct QueryData {\n            add_pull_request_review_thread_reply: Option<ReplyPayload>,\n        }\n\n        #[derive(Deserialize)]\n        struct ReplyPayload {\n            comment: Option<GraphQlReviewThreadComment>,\n        }\n\n        let data: QueryData = self\n            .graphql_query(GQL_ADD_REVIEW_THREAD_REPLY, &Variables { thread_id, body })\n            .await?;\n\n        let Some(comment) = data\n            .add_pull_request_review_thread_reply\n            .and_then(|payload| payload.comment)\n        else {\n            bail!(\"GitHub GraphQL addPullRequestReviewThreadReply returned no comment\");\n        };\n\n        Ok(comment.into())\n    }\n\n    /// List the diff-anchored review threads on a pull request, oldest\n    /// first.\n    ///\n    /// GraphQL rather than REST: `/pulls/{n}/comments` reports neither the\n    /// thread a comment belongs to nor whether that thread was resolved,\n    /// and a resolved thread must not read as still waiting on a reply.\n    pub async fn list_pull_request_review_threads(\n        &self,\n        owner: &str,\n        repo: &str,\n        pr_number: i64,\n    ) -> Result<Vec<PullRequestReviewThread>> {\n        #[derive(Serialize)]","sourceCodeStart":1250,"sourceCodeEnd":1286,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-github/src/client.rs#L1250-L1286","documentation":"Thrown by `add_pull_request_review_thread_reply` after the `addPullRequestReviewThreadReply` GraphQL mutation returns no comment in its payload. The reply was not created (or its payload was stripped), so the client cannot convert it to a `PullRequestComment` and bails.","triggerScenarios":"Replying to a review thread using an invalid/stale thread node id; the thread was resolved or deleted before the reply; GraphQL-level errors (permission denied on the repository, bad node id) yielding a null `comment`; the thread's PR was force-pushed and threads were regenerated.","commonSituations":"Caching thread ids from an earlier review session after the PR was rebased; replying on a private repo with a read-only token; two clients replying concurrently with one deleting the thread; GitHub UI removed outdated threads after new commits.","solutions":["Check the GraphQL `errors` array in the response for the real cause (invalid thread id vs permission).","Re-list the review threads (`list_review_threads`) to obtain fresh thread node ids before replying.","Confirm the token has write access to create review comments in the repo.","If the thread was resolved/deleted, post a regular PR comment instead of a thread reply.","Retry once after refreshing the thread id if the failure was transient."],"exampleFix":"// before\nlet reply = client.add_pull_request_review_thread_reply(&stale_thread_id, \"done\").await?;\n// after\nlet threads = client.list_pull_request_review_threads(owner, repo, pr_number).await?;\nif let Some(t) = threads.iter().find(|t| t.id == thread_id) {\n    let reply = client.add_pull_request_review_thread_reply(&t.node_id, \"done\").await?;\n}","handlingStrategy":"validation","validationCode":"let threads = client.list_pull_request_review_threads(owner, repo, pr).await?;\nif !threads.iter().any(|t| t.node_id == thread_id) {\n    return Err(anyhow!(\"thread {thread_id} no longer exists on {owner}/{repo}#{pr}\"));\n}\nif reply.trim().is_empty() { return Err(anyhow!(\"reply must not be empty\")); }","typeGuard":"fn thread_is_open(t: &PullRequestReviewThread) -> bool { !t.is_resolved && !t.is_outdated }","tryCatchPattern":"match client.add_pull_request_review_thread_reply(&thread_node_id, reply).await {\n    Ok(c) => c,\n    Err(e) if e.to_string().contains(\"no comment\") => {\n        // fall back to a regular PR comment\n        client.create_pull_request_comment(owner, repo, pr, reply).await?.into()\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Re-list threads before replying; ids go stale after force-pushes.","Fall back to a top-level PR comment when the thread is gone or resolved.","Ensure the token can create review comments (write access).","Skip replying to threads marked outdated instead of failing."],"tags":["github-api","graphql","mutation","review-thread"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}