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

  1. Check the GraphQL `errors` array in the response for the real cause (invalid thread id vs permission).
  2. Re-list the review threads (`list_review_threads`) to obtain fresh thread node ids before replying.
  3. Confirm the token has write access to create review comments in the repo.
  4. If the thread was resolved/deleted, post a regular PR comment instead of a thread reply.
  5. 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

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


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)