gitbutlerapp/gitbutler · error · anyhow::Error

Failed to decline pull request: {status} - {error_text}

Error message

Failed to decline pull request: {status} - {error_text}

What it means

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.

Source

Thrown at crates/but-bitbucket/src/client.rs:458

    /// Decline (close) a pull request.
    pub async fn decline_pull_request(
        &self,
        workspace: &str,
        repo_slug: &str,
        id: i64,
    ) -> Result<()> {
        let url = format!(
            "{}/repositories/{}/{}/pullrequests/{}/decline",
            self.base_url,
            urlencoding::encode(workspace),
            urlencoding::encode(repo_slug),
            id,
        );
        let response = self.client.post(&url).send().await?;
        if !response.status().is_success() {
            let status = response.status();
            let error_text = response.text().await.unwrap_or_default();
            bail!("Failed to decline pull request: {status} - {error_text}");
        }
        Ok(())
    }

    pub async fn fetch_repo(&self, workspace: &str, repo_slug: &str) -> Result<BitbucketRepo> {
        let url = format!(
            "{}/repositories/{}/{}",
            self.base_url,
            urlencoding::encode(workspace),
            urlencoding::encode(repo_slug),
        );
        let response = self.client.get(&url).send().await?;
        if !response.status().is_success() {
            let status = response.status();
            let error_text = response.text().await.unwrap_or_default();
            bail!("Failed to fetch repository: {status} - {error_text}");
        }
        let repo: BitbucketApiRepository = response.json().await?;

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Read {error_text} for the concrete reason; state errors mention the current PR state
  2. Re-fetch the PR first and only decline when values.state == OPEN
  3. Re-authenticate ('but config forge auth') if 401/403 so the token gains write scope
  4. Treat decline-of-already-closed PRs as a no-op success in automation
Defensive patterns

Strategy: try-catch

Validate before calling

let pr = client.get_pull_request(ws, slug, id).await?;
anyhow::ensure!(pr.state == "OPEN", "PR {id} already {}", pr.state);

Try / catch

match client.decline_pull_request(ws, slug, id).await {
    Err(e) if e.to_string().contains("409") => Ok(()), // already closed — desired end state reached
    r => r,
}

Prevention

When it happens

Trigger: 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).

Common situations: '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.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/f35955e1e819b23c. Report an issue: GitHub.