gitbutlerapp/gitbutler · error
GitHub GraphQL addPullRequestReviewThreadReply returned no…
Error message
GitHub GraphQL addPullRequestReviewThreadReply returned no comment
What it means
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.
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.
Example fix
// before
let reply = client.add_pull_request_review_thread_reply(&stale_thread_id, "done").await?;
// after
let threads = client.list_pull_request_review_threads(owner, repo, pr_number).await?;
if let Some(t) = threads.iter().find(|t| t.id == thread_id) {
let reply = client.add_pull_request_review_thread_reply(&t.node_id, "done").await?;
} Defensive patterns
Strategy: validation
Validate before calling
let threads = client.list_pull_request_review_threads(owner, repo, pr).await?;
if !threads.iter().any(|t| t.node_id == thread_id) {
return Err(anyhow!("thread {thread_id} no longer exists on {owner}/{repo}#{pr}"));
}
if reply.trim().is_empty() { return Err(anyhow!("reply must not be empty")); } Type guard
fn thread_is_open(t: &PullRequestReviewThread) -> bool { !t.is_resolved && !t.is_outdated } Try / catch
match client.add_pull_request_review_thread_reply(&thread_node_id, reply).await {
Ok(c) => c,
Err(e) if e.to_string().contains("no comment") => {
// fall back to a regular PR comment
client.create_pull_request_comment(owner, repo, pr, reply).await?.into()
}
Err(e) => return Err(e),
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.
Related errors
- GitHub GraphQL addReaction returned no reaction
- GitHub GraphQL removeReaction returned nothing
- GitHub did not change the review thread resolution
- Failed to create pull request comment
- Failed to delete comment
AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18).
Data as JSON: /api/errors/4639696faa4d30fd.
Report an issue: GitHub.
Appendix: source
Thrown at crates/but-github/src/client.rs:1268
#[serde(rename_all = "camelCase")]
struct QueryData {
add_pull_request_review_thread_reply: Option<ReplyPayload>,
}
#[derive(Deserialize)]
struct ReplyPayload {
comment: Option<GraphQlReviewThreadComment>,
}
let data: QueryData = self
.graphql_query(GQL_ADD_REVIEW_THREAD_REPLY, &Variables { thread_id, body })
.await?;
let Some(comment) = data
.add_pull_request_review_thread_reply
.and_then(|payload| payload.comment)
else {
bail!("GitHub GraphQL addPullRequestReviewThreadReply returned no comment");
};
Ok(comment.into())
}
/// List the diff-anchored review threads on a pull request, oldest
/// first.
///
/// GraphQL rather than REST: `/pulls/{n}/comments` reports neither the
/// thread a comment belongs to nor whether that thread was resolved,
/// and a resolved thread must not read as still waiting on a reply.
pub async fn list_pull_request_review_threads(
&self,
owner: &str,
repo: &str,
pr_number: i64,
) -> Result<Vec<PullRequestReviewThread>> {
#[derive(Serialize)]View on GitHub (pinned to 58e5313667)