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

  1. Read the embedded error_text — for 404 verify the project id and MR iid are correct and the MR still exists.
  2. For 401/403, check the token's scopes (api scope required) and the user's role in the project.
  3. 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.
  4. 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

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


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)