gitbutlerapp/gitbutler · error · HttpStatusError

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

Error message

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

What it means

Raised by but-bitbucket's pull-request creation: the POST to Bitbucket Cloud's /2.0/repositories/{workspace}/{repo_slug}/pullrequests answered a non-success HTTP status. The error wraps HttpStatusError with the status code plus Bitbucket's raw response body (error_text), which names the exact violation. Note the client already retries once with an empty reviewer list when a 400 was caused by default reviewers, so a surfaced failure usually has another cause (scope, duplicate PR, credentials).

Source

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

            close_source_branch: true,
            source,
            destination: DestinationBody {
                branch: BranchBody {
                    name: params.target_branch,
                },
            },
            reviewers: reviewer_account_ids
                .iter()
                .map(|account_id| ReviewerBody { account_id })
                .collect(),
        };

        let response = self.client.post(&url).json(&body).send().await?;
        if !response.status().is_success() {
            let status = response.status();
            let error_text = response.text().await.unwrap_or_default();
            return Err(
                anyhow::Error::new(HttpStatusError { status }).context(format!(
                    "Failed to create pull request: {status} - {error_text}"
                )),
            );
        }
        let pr: BitbucketApiPullRequest = response.json().await?;
        Ok(pr.into())
    }

    /// Bitbucket does not apply default reviewers to API-created pull requests,
    /// so fetch the effective set (repository- plus project-level) minus the
    /// author, whom Bitbucket rejects as a reviewer.
    /// Best-effort: a failed lookup yields no reviewers.
    async fn default_reviewers_for_new_pr(&self, workspace: &str, repo_slug: &str) -> Vec<String> {
        #[derive(Deserialize)]
        struct DefaultReviewer {
            #[serde(default)]
            user: Option<BitbucketApiUser>,
        }

View on GitHub (pinned to 2497b8007a)

Solutions

  1. Read the {error_text} segment of the message first - Bitbucket names the offending field (e.g. reviewers or source branch)
  2. Recreate the token with the pullrequest:write scope (plus read:user:bitbucket for reviewer lookup) and re-add it
  3. Check whether an open PR for the same source/target pair already exists and reuse or close it before creating another
  4. Verify workspace and repo_slug against the browser URL (bitbucket.org/<workspace>/<repo_slug>)
  5. On 429/5xx, honor Retry-After and re-run the command after a pause

Example fix

// before
if !response.status().is_success() {
    let status = response.status();
    let error_text = response.text().await.unwrap_or_default();
    return Err(anyhow::Error::new(HttpStatusError { status })
        .context(format!("Failed to create pull request: {status} - {error_text}")));
}

// after - classify the frequent 400 duplicate-PR case into an actionable message
if status == reqwest::StatusCode::BAD_REQUEST && error_text.contains("already") {
    return Err(anyhow!("An open pull request for this source/target pair already exists"));
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before creating, confirm no open PR exists for the same source/target pair
let open = client
    .get(format!("{base}/repositories/{ws}/{slug}/pullrequests?state=OPEN&q=source.branch.name=\"{src}\"+AND+destination.branch.name=\"{dst}\""))
    .send().await?
    .json::<BitbucketPage>().await?;
if !open.values.is_empty() {
    anyhow::bail!("an open pull request for {src} -> {dst} already exists");
}

Type guard

fn is_http_status_error(err: &anyhow::Error) -> bool {
    err.downcast_ref::<HttpStatusError>().is_some()
}

Try / catch

match client.create_pull_request(&params).await {
    Ok(pr) => { /* store pr.id */ }
    Err(err) => {
        if let Some(e) = err.downcast_ref::<HttpStatusError>() {
            match e.status {
                reqwest::StatusCode::FORBIDDEN => /* guide user to add pullrequest:write scope */,
                reqwest::StatusCode::UNAUTHORIZED => /* prompt re-auth */,
                reqwest::StatusCode::NOT_FOUND => /* re-check workspace/repo_slug */,
                s if s.is_client_error() => /* surface err's context: status + error_text */,
                _ => /* transient: schedule retry with backoff */,
            }
        } else {
            return Err(err);
        }
    }
}

Prevention

When it happens

Trigger: Calling the PR-creating flow with a token lacking the pullrequest:write scope (403); an open pull request already exists for the same source branch -> target branch pair (400); expired or invalid credentials (401); wrong workspace or repo_slug (404); a reviewer rejected for a reason other than being the author (400); API rate limiting (429).

Common situations: Workspace-scoped API key created with read-only scopes; re-running a create-PR command that already succeeded; copying the repo slug with wrong casing; project-level default reviewers that include deactivated accounts.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17). Data as JSON: /api/errors/247f6e1566cbea39. Report an issue: GitHub.