gitbutlerapp/gitbutler · error

Failed to decline pull request

Error message

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

What it means

Thrown by decline_pull_request when the HTTP POST that declines (closes without merging) a Bitbucket pull request returns a non-success status. The message includes the HTTP status and Bitbucket's error body. Most commonly the PR is no longer in a state that can be declined.

Solutions

  1. Fetch the PR's current state first and skip declining if already merged/declined
  2. Verify the PR id is correct and the PR is still open
  3. Check the token has write permission on the repository
  4. Treat 'already closed' style error bodies as success in idempotent automation

Example fix

// before
client.decline_pull_request(workspace, repo_slug, id).await?;
// after
match client.decline_pull_request(workspace, repo_slug, id).await {
    Ok(()) => {},
    Err(e) if e.to_string().contains("400") => { /* already merged/declined; ignore */ },
    Err(e) => return Err(e),
}
Defensive patterns

Strategy: try-catch

Validate before calling

let pr_state = get_pr_state(ws, slug, id)?;
anyhow::ensure!(pr_state == "OPEN", "PR #{id} already closed; skip decline");

Try / catch

match client.decline_pull_request(ws, slug, id).await {
    Err(e) if e.to_string().contains("400") || e.to_string().contains("409") => { /* already merged/declined */ },
    other => other?,
}

Prevention

When it happens

Trigger: Declining a PR that was already merged or already declined (400/409), wrong PR id (404), or a token lacking write access (403/401).

Common situations: Race with another teammate who merged/closed the PR first; stale cached PR id; automation retrying decline on an already-closed PR; bot token with read-only scope.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/f35955e1e819b23c. Report an issue: GitHub.

Appendix: source

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

    /// 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 58e5313667)