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
- Log/inspect the GraphQL `errors` array returned alongside the data — it usually names the failing node id or permission problem.
- Re-fetch the review via `list_pull_request_reviews` to get a fresh node id before retrying the mutation.
- Verify the reaction `content` enum value is valid for reviews (e.g. THUMBS_UP, HEART).
- Check the token has write access to the repository's conversations.
- 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
- Always fetch fresh review node ids instead of caching them across sessions.
- Log the GraphQL errors array, not just the bail message.
- Restrict reaction content to the supported enum values.
- Check review existence before reacting.
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
- GitHub GraphQL removeReaction returned nothing
- 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/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)