{"record":{"id":"f35955e1e819b23c","repo":"gitbutlerapp/gitbutler","slug":"failed-to-decline-pull-request-status-error","errorCode":null,"errorMessage":"Failed to decline pull request: {status} - {error_text}","messagePattern":"Failed to decline pull request: (.+?) - (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-bitbucket/src/client.rs","lineNumber":458,"sourceCode":"    /// Decline (close) a pull request.\n    pub async fn decline_pull_request(\n        &self,\n        workspace: &str,\n        repo_slug: &str,\n        id: i64,\n    ) -> Result<()> {\n        let url = format!(\n            \"{}/repositories/{}/{}/pullrequests/{}/decline\",\n            self.base_url,\n            urlencoding::encode(workspace),\n            urlencoding::encode(repo_slug),\n            id,\n        );\n        let response = self.client.post(&url).send().await?;\n        if !response.status().is_success() {\n            let status = response.status();\n            let error_text = response.text().await.unwrap_or_default();\n            bail!(\"Failed to decline pull request: {status} - {error_text}\");\n        }\n        Ok(())\n    }\n\n    pub async fn fetch_repo(&self, workspace: &str, repo_slug: &str) -> Result<BitbucketRepo> {\n        let url = format!(\n            \"{}/repositories/{}/{}\",\n            self.base_url,\n            urlencoding::encode(workspace),\n            urlencoding::encode(repo_slug),\n        );\n        let response = self.client.get(&url).send().await?;\n        if !response.status().is_success() {\n            let status = response.status();\n            let error_text = response.text().await.unwrap_or_default();\n            bail!(\"Failed to fetch repository: {status} - {error_text}\");\n        }\n        let repo: BitbucketApiRepository = response.json().await?;","sourceCodeStart":440,"sourceCodeEnd":476,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-bitbucket/src/client.rs#L440-L476","documentation":"Raised when POSTing to /repositories/{workspace}/{repo_slug}/pullrequests/{id}/decline returns non-2xx; the message embeds the status and the raw response body. Declining is only valid on an OPEN pull request with a token that has pullrequest write permission.","triggerScenarios":"Declining a PR that is already MERGED (Bitbucket refuses to decline merged PRs), already DECLINED (409 state conflict), a deleted PR (404), or using a token without write scope (401/403).","commonSituations":"'Close all my PRs' batch scripts that race with auto-merge; stale UI views letting users decline an already-merged PR; read-only service tokens used for lifecycle operations.","solutions":["Read {error_text} for the concrete reason; state errors mention the current PR state","Re-fetch the PR first and only decline when values.state == OPEN","Re-authenticate ('but config forge auth') if 401/403 so the token gains write scope","Treat decline-of-already-closed PRs as a no-op success in automation"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"let pr = client.get_pull_request(ws, slug, id).await?;\nanyhow::ensure!(pr.state == \"OPEN\", \"PR {id} already {}\", pr.state);","typeGuard":null,"tryCatchPattern":"match client.decline_pull_request(ws, slug, id).await {\n    Err(e) if e.to_string().contains(\"409\") => Ok(()), // already closed — desired end state reached\n    r => r,\n}","preventionTips":["Treat decline as idempotent in automation: refresh state on 409 instead of failing","Filter lists to OPEN before offering bulk-decline actions","Re-check state after any long-running workflow before declining"],"tags":["bitbucket","pull-request","decline","http"],"backgroundTag":"pull-request-state-conflict","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}