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
- Inspect the GraphQL `errors` payload returned before the null data for the precise cause.
- Re-fetch the review's reactions to confirm the reaction id still exists; treat 'reaction absent' as success (idempotent removal).
- Refresh the review node id via `list_pull_request_reviews` and retry once.
- Verify the token still has write access to the repo's conversations.
- 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
- Verify the reaction still exists before removing (idempotency).
- Refresh node ids after any review reload.
- Handle concurrent removal by other clients gracefully.
- Inspect GraphQL errors to distinguish 'already gone' from permission failures.
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
- GitHub GraphQL addReaction returned no reaction
- GitHub GraphQL addPullRequestReviewThreadReply returned no…
- Failed to create pull request comment
- Failed to delete comment
- Failed to request reviewers
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)