{"record":{"id":"f73eae98f5b40ecf","repo":"gitbutlerapp/gitbutler","slug":"github-graphql-removereaction-returned-nothing","errorCode":null,"errorMessage":"GitHub GraphQL removeReaction returned nothing","messagePattern":"GitHub GraphQL removeReaction returned nothing","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-github/src/client.rs","lineNumber":1190,"sourceCode":"        struct QueryData {\n            remove_reaction: Option<serde::de::IgnoredAny>,\n        }\n\n        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_REMOVE_REACTION,\n                &ReactionVariables {\n                    subject_id: &subject_id,\n                    content: graphql_reaction_content(content)?,\n                },\n            )\n            .await?;\n\n        if data.remove_reaction.is_none() {\n            bail!(\"GitHub GraphQL removeReaction returned nothing\");\n        }\n        Ok(())\n    }\n\n    /// Set the resolution state of a review conversation on GitHub.\n    pub async fn set_review_thread_resolved(&self, thread_id: &str, resolved: bool) -> Result<()> {\n        #[derive(Serialize)]\n        #[serde(rename_all = \"camelCase\")]\n        struct Variables<'a> {\n            thread_id: &'a str,\n        }\n\n        #[derive(Deserialize)]\n        struct QueryData {\n            result: Option<Payload>,\n        }\n\n        #[derive(Deserialize)]","sourceCodeStart":1172,"sourceCodeEnd":1208,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-github/src/client.rs#L1172-L1208","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nclient.remove_pull_request_review_reaction(&review_id, ReactionContent::Heart).await?; // bails if already gone\n// after\nlet has = client.review_has_reaction(&review_id, ReactionContent::Heart).await?;\nif has {\n    client.remove_pull_request_review_reaction(&review_id, ReactionContent::Heart).await?;\n} // treat absence as already-removed","handlingStrategy":"try-catch","validationCode":"let reactions = client.list_review_reactions(owner, repo, pr, review_db_id).await?;\nif !reactions.iter().any(|r| r.content == content && r.is_mine) {\n    return Ok(()); // nothing to remove: idempotent no-op\n}","typeGuard":"fn remove_reaction_present(payload: &Option<RemoveReactionPayload>) -> bool { payload.is_some() }","tryCatchPattern":"match client.remove_pull_request_review_reaction(&node_id, content).await {\n    Ok(()) => {},\n    Err(e) if e.to_string().contains(\"returned nothing\") => {}, // likely already removed\n    Err(e) => return Err(e),\n}","preventionTips":["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."],"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"}