gitbutlerapp/gitbutler · error · anyhow::Error
Failed to get MR merge status: {}
Error message
Failed to get MR merge status: {} What it means
Thrown by GitLabClient::get_mr_merge_status when GET /projects/:id/merge_requests/:iid (the same endpoint used for full MR fetches) returns a non-2xx status while probing only the merge_status and user_notes_count fields. It exists as a cheap pre-check before attempting a merge: is_mergeable is true only when GitLab reports merge_status == "can_be_merged". Like the other status bails in this client, only the status code is reported, not GitLab's error body.
Source
Thrown at crates/but-gitlab/src/client.rs:349
&self,
project_id: GitLabProjectId,
mr_iid: i64,
) -> Result<MergeRequestMergeStatus> {
#[derive(Debug, Deserialize)]
struct MrMergeStatusResponse {
#[serde(default)]
merge_status: Option<String>,
#[serde(default)]
user_notes_count: i64,
}
let url = format!(
"{}/projects/{}/merge_requests/{}",
self.base_url, project_id, mr_iid
);
let response = self.client.get(&url).send().await?;
if !response.status().is_success() {
bail!("Failed to get MR merge status: {}", response.status());
}
let body: MrMergeStatusResponse = response.json().await?;
let is_mergeable = matches!(body.merge_status.as_deref(), Some("can_be_merged"));
Ok(MergeRequestMergeStatus {
mergeable_state: body.merge_status,
comments_count: body.user_notes_count,
is_mergeable,
})
}
pub async fn update_merge_request(
&self,
params: &UpdateMergeRequestParams<'_>,
) -> Result<MergeRequest> {
#[derive(Serialize)]
struct UpdateMergeRequestBody<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<&'a str>,View on GitHub (pinned to caf1f223d3)
Solutions
- Verify the MR still exists (browser or curl the same endpoint) before polling status
- Re-run `but config forge auth` if the token expired (401)
- Stop or debounce the polling loop once an MR is detected as closed/merged so it does not query deleted MRs
- If 403, confirm the account still has access to the project
Example fix
// before
let status = client.get_mr_merge_status(project_id, mr_iid).await?; // error bubbles into the whole view
// after
let status = match client.get_mr_merge_status(project_id, mr_iid).await {
Ok(s) => s,
Err(err) if err.to_string().contains("404") => {
MergeRequestMergeStatus { mergeable_state: None, comments_count: 0, is_mergeable: false } // MR gone; not mergeable
}
Err(err) => return Err(err),
}; Defensive patterns
Strategy: try-catch
Validate before calling
// Only poll status for MRs you know exist (from a prior list/get)
ensure!(known_mr_ids.contains(&mr_iid), "skipping status poll for unknown MR {mr_iid}");
let status = client.get_mr_merge_status(project_id, mr_iid).await?; Try / catch
let mergeable = match client.get_mr_merge_status(project_id, mr_iid).await {
Ok(s) => s.is_mergeable,
Err(_) => false, // unknown state is not mergeable; never enable merge actions on it
}; Prevention
- Drive merge-button enablement from is_mergeable instead of assuming, so 405 merges never happen
- Debounce or stop status polling once an MR leaves the opened state
When it happens
Trigger: Calling get_mr_merge_status(project_id, mr_iid) for an MR that was deleted or is not visible to the token (404), a forbidden project (403), an unauthenticated request (401), or passing mr_iid = 0 / a negative value that GitLab rejects.
Common situations: UI polls merge status on a timer for an MR that got closed+deleted while the poll was in flight; token lost permissions when the user was removed from the project; stale MR list cached locally referencing deleted MRs.
Related errors
- Failed to get merge request: {}
- Failed to update merge request: {status} - {error_text}
- Failed to merge merge request: {}
- Failed to set merge request draft state: {status} - {error_t
- Failed to enable merge request auto-merge: {status} - {error
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/9bdb7cc2a8954fd8.
Report an issue: GitHub.