{"record":{"id":"cda9b1ec626a55c4","repo":"gitbutlerapp/gitbutler","slug":"github-graphql-addreaction-returned-no-reaction","errorCode":null,"errorMessage":"GitHub GraphQL addReaction returned no reaction","messagePattern":"GitHub GraphQL addReaction returned no reaction","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-github/src/client.rs","lineNumber":1156,"sourceCode":"        let subject_id = self\n            .get_pull_request_review_node_id(owner, repo, pr_number, review_id)\n            .await?;\n        let data: QueryData = self\n            .graphql_query(\n                GQL_ADD_REACTION,\n                &ReactionVariables {\n                    subject_id: &subject_id,\n                    content: graphql_reaction_content(content)?,\n                },\n            )\n            .await?;\n\n        let Some(reaction) = data\n            .add_reaction\n            .and_then(|payload| payload.reaction)\n            .and_then(GraphQlReaction::into_reaction)\n        else {\n            bail!(\"GitHub GraphQL addReaction returned no reaction\");\n        };\n        Ok(reaction)\n    }\n\n    /// Remove the caller's reaction of one kind from one submitted review.\n    pub async fn remove_pull_request_review_reaction(\n        &self,\n        owner: &str,\n        repo: &str,\n        pr_number: i64,\n        review_id: i64,\n        content: &str,\n    ) -> Result<()> {\n        #[derive(Deserialize)]\n        #[serde(rename_all = \"camelCase\")]\n        struct QueryData {\n            remove_reaction: Option<serde::de::IgnoredAny>,\n        }","sourceCodeStart":1138,"sourceCodeEnd":1174,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-github/src/client.rs#L1138-L1174","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nlet reaction = client.add_pull_request_review_reaction(&review_node_id, ReactionContent::ThumbsUp).await?;\n// after\nlet review = client.get_pull_request_review_node(&review_node_id).await?; // refresh id\nmatch client.add_pull_request_review_reaction(&review.id, ReactionContent::ThumbsUp).await {\n    Ok(r) => r,\n    Err(e) => { tracing::warn!(\"addReaction failed: {e}\"); return Err(e); }\n}","handlingStrategy":"try-catch","validationCode":"let review = client.get_pull_request_review_node(owner, repo, pr, review_db_id).await?; // fresh node id\nlet valid = matches!(content, ReactionContent::ThumbsUp | ReactionContent::ThumbsDown | ReactionContent::Heart | ReactionContent::Eyes | ReactionContent::Rocket | ReactionContent::Laugh | ReactionContent::Confused);\nif !valid { return Err(anyhow!(\"unsupported reaction content\")); }","typeGuard":"fn has_reaction_payload(payload: &Option<AddReactionPayload>) -> bool {\n    payload.as_ref().and_then(|p| p.reaction.as_ref()).is_some()\n}","tryCatchPattern":"match client.add_pull_request_review_reaction(&node_id, content).await {\n    Ok(r) => r,\n    Err(e) => {\n        tracing::warn!(\"addReaction failed for {node_id}: {e}\");\n        return Err(e);\n    }\n}","preventionTips":["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."],"tags":["github-api","graphql","mutation","reaction"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}