gitbutlerapp/gitbutler · error
Failed to merge merge request
Error message
Failed to merge merge request: {} What it means
GitLabClient::merge_merge_request sends a PUT to the GitLab merge-request merge endpoint and treats any non-2xx HTTP status from GitLab as a failure. The GitLab API rejected the merge request; the response status (e.g. 405, 406, 409, 422) is included in the message. The error body text is NOT captured here, so the message only reveals the status code.
Solutions
- Check the status code in the message: 405 usually means the MR is not mergeable (check `merge_status` / `detailed_merge_status` first), 409 means sha mismatch or already merged.
- Re-fetch the MR and verify it is mergeable (no unresolved threads, pipeline green, not draft) before calling merge_merge_request.
- Pass the correct `sha` parameter (the head commit at fetch time) so GitLab can reject stale merges explicitly instead of silently merging; if 409, re-fetch and retry.
- Verify the authenticated token has at least Developer role and merge rights on the target branch (protected branch rules).
- Improve diagnosability by reading the response body like the sibling methods do (response.text()) and including it in the error.
Example fix
// before
let response = self.client.put(&url).json(&body).send().await?;
if !response.status().is_success() {
bail!("Failed to merge merge request: {}", response.status());
}
// after
let response = self.client.put(&url).json(&body).send().await?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
bail!("Failed to merge merge request: {status} - {error_text}");
} Defensive patterns
Strategy: try-catch
Validate before calling
let mr = client.get_merge_request(project_id, mr_iid).await?; assert_eq!(mr.draft, false); assert!(mr.merge_status == "can_be_merged"); assert!(mr.blocking_discussions_resolved == Some(true));
Try / catch
match client.merge_merge_request(project_id, mr_iid, params).await {
Ok(()) => println!("merged"),
Err(e) => {
let msg = e.to_string();
if msg.contains("409") {
// stale sha or already merged: re-fetch MR and retry once
} else if msg.contains("405") {
// not mergeable: check pipeline/discussions
} else {
return Err(e);
}
}
} Prevention
- Check detailed_merge_status and blocking_discussions_resolved before merging
- Always pass the sha you fetched the MR with so stale merges fail fast
- Log the response body (patch the client or wrap the call) for actionable GitLab errors
- Verify branch protection rules allow the token's user to merge the target branch
When it happens
Trigger: Calling merge_merge_request when GitLab returns a non-success status: MR is not mergeable (unresolved discussions, failing pipelines/merge checks), MR already merged or closed, insufficient permissions to merge, sha mismatch (MR was updated since fetched), or squash/merge params invalid for the project settings.
Common situations: Merging an MR whose branch was just updated by another commit (stale sha); CI pipeline still running with 'Pipelines must succeed' enabled; user lacks Developer+ role; MR in draft state when project blocks merging drafts; MR already merged by someone else.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- Failed to create merge request
- Failed to disable merge request auto-merge
- Failed to enable merge request auto-merge
- Failed to fetch project
- Failed to set merge request draft state
AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18).
Data as JSON: /api/errors/2df720318c1648b0.
Report an issue: GitHub.
Appendix: source
Thrown at crates/but-gitlab/src/client.rs:494
#[derive(Serialize)]
struct MergeMergeRequestBody {
#[serde(skip_serializing_if = "Option::is_none")]
squash: Option<bool>,
}
let url = format!(
"{}/projects/{}/merge_requests/{}/merge",
self.base_url, params.project_id, params.mr_iid
);
let body = MergeMergeRequestBody {
squash: params.squash,
};
let response = self.client.put(&url).json(&body).send().await?;
if !response.status().is_success() {
bail!("Failed to merge merge request: {}", response.status());
}
Ok(())
}
pub async fn set_merge_request_draft_state(
&self,
params: &SetMergeRequestDraftStateParams,
) -> Result<()> {
let project_id = params.project_id.clone();
let mr = self
.get_merge_request(project_id.clone(), params.mr_iid)
.await?;
let next_title = update_draft_state_in_title(&mr.title, params.is_draft);
if next_title == mr.title {
return Ok(());
}View on GitHub (pinned to 58e5313667)