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
- Read the {error_text} segment of the message first - Bitbucket names the offending field (e.g. reviewers or source branch)
- Recreate the token with the pullrequest:write scope (plus read:user:bitbucket for reviewer lookup) and re-add it
- Check whether an open PR for the same source/target pair already exists and reuse or close it before creating another
- Verify workspace and repo_slug against the browser URL (bitbucket.org/<workspace>/<repo_slug>)
- 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(¶ms).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
- Create tokens with a written scope checklist (pullrequest:write, read:user, read:repository) before first use
- Make PR-creation scripts idempotent: query open PRs for the pair before POSTing
- Log the full error chain (status plus Bitbucket's error body) so reruns are diagnosable
- Respect Retry-After on 429 instead of immediately retrying
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
- Failed to verify Bitbucket repository access
- Bitbucket repository '{workspace}/{repo_slug}' is inaccessib
- HTTP Error ${response.statusText}: ${text}
- Failed to persist action: {e}
- Failed to list actions: {e}
AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17).
Data as JSON: /api/errors/247f6e1566cbea39.
Report an issue: GitHub.