gitbutlerapp/gitbutler · error
Failed to set merge request draft state
Error message
Failed to set merge request draft state: {status} - {error_text} What it means
set_merge_request_draft_state toggles an MR between draft and ready via the GitLab API. Any non-2xx response triggers this bail, embedding both the HTTP status and GitLab's JSON error body, so the message usually contains GitLab's own reason (e.g. 403 insufficient permissions, 404 wrong MR iid).
Solutions
- Read the embedded error_text — for 404 verify the project id and MR iid are correct and the MR still exists.
- For 401/403, check the token's scopes (api scope required) and the user's role in the project.
- Confirm the MR exists and get its current draft state via the API before toggling to avoid redundant no-op calls that may still fail.
- Handle 429/5xx with a retry with backoff since these are transient server-side conditions.
Example fix
// before
match client.set_merge_request_draft_state(project_id, iid, false).await {
Ok(()) => {}
Err(e) => panic!("{e}"),
}
// after
match client.set_merge_request_draft_state(project_id, iid, false).await {
Ok(()) => {}
Err(e) if e.to_string().contains("404") => {
// MR gone; refresh MR list before retrying
}
Err(e) => return Err(e),
} Defensive patterns
Strategy: try-catch
Validate before calling
let mr = client.get_merge_request(project_id, mr_iid).await?;
// only toggle when state differs and MR is open
if mr.state == "opened" && mr.draft != target_draft {
client.set_merge_request_draft_state(project_id, mr_iid, target_draft).await?;
} Try / catch
match client.set_merge_request_draft_state(project_id, iid, true).await {
Ok(()) => {}
Err(e) if e.to_string().contains("404") => eprintln!("MR no longer exists, skipping"),
Err(e) if e.to_string().contains("401") || e.to_string().contains("403") => {
return Err(anyhow!("token lacks permission: {e}"))
}
Err(e) => return Err(e),
} Prevention
- Verify the MR iid belongs to the same project id used in the call
- Ensure the token has the api scope and the user has at least Developer role
- Check MR state == "opened" before toggling draft state
- Retry transient statuses (429, 5xx) with exponential backoff
When it happens
Trigger: Calling set_merge_request_draft_state with an MR iid that does not exist in the project (404), a token lacking push/merge permissions (403/401), an invalid or expired token (401), or a malformed request body rejected by GitLab (400/422).
Common situations: MR was deleted or iid came from a different project; token scope changed after creation; user tries to mark ready an MR owned by another user without permissions; GitLab instance is a version that rejects certain title changes.
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 disable merge request auto-merge
- Failed to enable merge request auto-merge
- Failed to merge merge request
- Failed to create merge request
- Failed to fetch project
AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18).
Data as JSON: /api/errors/eafc323cc2d1f9b3.
Report an issue: GitHub.
Appendix: source
Thrown at crates/but-gitlab/src/client.rs:534
title: &'a str,
}
let url = format!(
"{}/projects/{}/merge_requests/{}",
self.base_url, project_id, params.mr_iid
);
let response = self
.client
.put(&url)
.json(&UpdateMergeRequestBody { title: &next_title })
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
bail!("Failed to set merge request draft state: {status} - {error_text}");
}
Ok(())
}
pub async fn set_merge_request_auto_merge(
&self,
params: &SetMergeRequestAutoMergeParams,
) -> Result<()> {
let project_id = params.project_id.clone();
if params.enabled {
#[derive(Serialize)]
struct EnableAutoMergeBody {
auto_merge: bool,
}
let url = format!(
"{}/projects/{}/merge_requests/{}/merge",View on GitHub (pinned to 58e5313667)