gitbutlerapp/gitbutler · error

GitHub GraphQL removeReaction returned nothing

Error message

GitHub GraphQL removeReaction returned nothing

What it means

Thrown by `remove_pull_request_review_reaction` after the `removeReaction` GraphQL mutation returns `data.remove_reaction: null`. The mutation produced no payload, so the client cannot confirm the reaction was actually removed and bails.

Solutions

  1. Inspect the GraphQL `errors` payload returned before the null data for the precise cause.
  2. Re-fetch the review's reactions to confirm the reaction id still exists; treat 'reaction absent' as success (idempotent removal).
  3. Refresh the review node id via `list_pull_request_reviews` and retry once.
  4. Verify the token still has write access to the repo's conversations.
  5. Ensure the reaction `content` matches the reaction being removed.

Example fix

// before
client.remove_pull_request_review_reaction(&review_id, ReactionContent::Heart).await?; // bails if already gone
// after
let has = client.review_has_reaction(&review_id, ReactionContent::Heart).await?;
if has {
    client.remove_pull_request_review_reaction(&review_id, ReactionContent::Heart).await?;
} // treat absence as already-removed
Defensive patterns

Strategy: try-catch

Validate before calling

let reactions = client.list_review_reactions(owner, repo, pr, review_db_id).await?;
if !reactions.iter().any(|r| r.content == content && r.is_mine) {
    return Ok(()); // nothing to remove: idempotent no-op
}

Type guard

fn remove_reaction_present(payload: &Option<RemoveReactionPayload>) -> bool { payload.is_some() }

Try / catch

match client.remove_pull_request_review_reaction(&node_id, content).await {
    Ok(()) => {},
    Err(e) if e.to_string().contains("returned nothing") => {}, // likely already removed
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling `remove_pull_request_review_reaction` with a reaction node id that no longer exists, a stale review id, a GraphQL-level error making the payload null, or passing a content type that does not match the existing reaction.

Common situations: Another user/device already removed the reaction, so the id is stale; reaction ids cached across sessions; the review itself was deleted concurrently; token lost conversation-write permission.

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

Appendix: source

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

        struct QueryData {
            remove_reaction: Option<serde::de::IgnoredAny>,
        }

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

        if data.remove_reaction.is_none() {
            bail!("GitHub GraphQL removeReaction returned nothing");
        }
        Ok(())
    }

    /// Set the resolution state of a review conversation on GitHub.
    pub async fn set_review_thread_resolved(&self, thread_id: &str, resolved: bool) -> Result<()> {
        #[derive(Serialize)]
        #[serde(rename_all = "camelCase")]
        struct Variables<'a> {
            thread_id: &'a str,
        }

        #[derive(Deserialize)]
        struct QueryData {
            result: Option<Payload>,
        }

        #[derive(Deserialize)]

View on GitHub (pinned to 58e5313667)