gitbutlerapp/gitbutler · error

GitHub GraphQL addReaction returned no reaction

Error message

GitHub GraphQL addReaction returned no reaction

What it means

Thrown by `add_pull_request_review_reaction` after running the `addReaction` GraphQL mutation: the response deserialized, but `data.add_reaction.reaction` was null (or failed conversion), so no reaction object came back. It means GitHub accepted the request shape but did not produce the expected reaction payload.

Solutions

  1. Log/inspect the GraphQL `errors` array returned alongside the data — it usually names the failing node id or permission problem.
  2. Re-fetch the review via `list_pull_request_reviews` to get a fresh node id before retrying the mutation.
  3. Verify the reaction `content` enum value is valid for reviews (e.g. THUMBS_UP, HEART).
  4. Check the token has write access to the repository's conversations.
  5. If the review was deleted, skip reacting instead of retrying.

Example fix

// before
let reaction = client.add_pull_request_review_reaction(&review_node_id, ReactionContent::ThumbsUp).await?;
// after
let review = client.get_pull_request_review_node(&review_node_id).await?; // refresh id
match client.add_pull_request_review_reaction(&review.id, ReactionContent::ThumbsUp).await {
    Ok(r) => r,
    Err(e) => { tracing::warn!("addReaction failed: {e}"); return Err(e); }
}
Defensive patterns

Strategy: try-catch

Validate before calling

let review = client.get_pull_request_review_node(owner, repo, pr, review_db_id).await?; // fresh node id
let valid = matches!(content, ReactionContent::ThumbsUp | ReactionContent::ThumbsDown | ReactionContent::Heart | ReactionContent::Eyes | ReactionContent::Rocket | ReactionContent::Laugh | ReactionContent::Confused);
if !valid { return Err(anyhow!("unsupported reaction content")); }

Type guard

fn has_reaction_payload(payload: &Option<AddReactionPayload>) -> bool {
    payload.as_ref().and_then(|p| p.reaction.as_ref()).is_some()
}

Try / catch

match client.add_pull_request_review_reaction(&node_id, content).await {
    Ok(r) => r,
    Err(e) => {
        tracing::warn!("addReaction failed for {node_id}: {e}");
        return Err(e);
    }
}

Prevention

When it happens

Trigger: Calling `add_pull_request_review_reaction` with an invalid or stale review node id, reacting with content GitHub rejected, or the mutation returning a top-level GraphQL `errors` array while `data.add_reaction` is null (e.g. insufficient permission on the review, or the review was deleted).

Common situations: Reacting to a review whose node id was cached from an earlier session and has since been deleted; user lacks read/write on the PR thread; the review belongs to a private repo the token cannot see; GitHub schema drift after the mutation renamed fields.

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/cda9b1ec626a55c4. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-github/src/client.rs:1156

        let subject_id = self
            .get_pull_request_review_node_id(owner, repo, pr_number, review_id)
            .await?;
        let data: QueryData = self
            .graphql_query(
                GQL_ADD_REACTION,
                &ReactionVariables {
                    subject_id: &subject_id,
                    content: graphql_reaction_content(content)?,
                },
            )
            .await?;

        let Some(reaction) = data
            .add_reaction
            .and_then(|payload| payload.reaction)
            .and_then(GraphQlReaction::into_reaction)
        else {
            bail!("GitHub GraphQL addReaction returned no reaction");
        };
        Ok(reaction)
    }

    /// Remove the caller's reaction of one kind from one submitted review.
    pub async fn remove_pull_request_review_reaction(
        &self,
        owner: &str,
        repo: &str,
        pr_number: i64,
        review_id: i64,
        content: &str,
    ) -> Result<()> {
        #[derive(Deserialize)]
        #[serde(rename_all = "camelCase")]
        struct QueryData {
            remove_reaction: Option<serde::de::IgnoredAny>,
        }

View on GitHub (pinned to 58e5313667)