{"record":{"id":"eafc323cc2d1f9b3","repo":"gitbutlerapp/gitbutler","slug":"failed-to-set-merge-request-draft-state-status","errorCode":null,"errorMessage":"Failed to set merge request draft state: {status} - {error_text}","messagePattern":"Failed to set merge request draft state: (.+?) - (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-gitlab/src/client.rs","lineNumber":468,"sourceCode":"            title: &'a str,\n        }\n\n        let url = format!(\n            \"{}/projects/{}/merge_requests/{}\",\n            self.base_url, project_id, params.mr_iid\n        );\n\n        let response = self\n            .client\n            .put(&url)\n            .json(&UpdateMergeRequestBody { title: &next_title })\n            .send()\n            .await?;\n\n        if !response.status().is_success() {\n            let status = response.status();\n            let error_text = response.text().await.unwrap_or_default();\n            bail!(\"Failed to set merge request draft state: {status} - {error_text}\");\n        }\n\n        Ok(())\n    }\n\n    pub async fn set_merge_request_auto_merge(\n        &self,\n        params: &SetMergeRequestAutoMergeParams,\n    ) -> Result<()> {\n        let project_id = params.project_id.clone();\n        if params.enabled {\n            #[derive(Serialize)]\n            struct EnableAutoMergeBody {\n                auto_merge: bool,\n            }\n\n            let url = format!(\n                \"{}/projects/{}/merge_requests/{}/merge\",","sourceCodeStart":450,"sourceCodeEnd":486,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-gitlab/src/client.rs#L450-L486","documentation":"Thrown by GitLabClient::set_merge_request_draft_state when the underlying MR title update (PUT /projects/:id/merge_requests/:iid with only the title field) fails. The function works by rewriting the title: it prepends or strips the `Draft: ` prefix (see update_draft_state_in_title) rather than using a dedicated endpoint. The bail includes both the HTTP status and GitLab's response body, so the validation reason is usually readable in the message.","triggerScenarios":"Toggling draft state on an MR that was deleted (404); the stripped/rewritten title becomes empty or invalid and GitLab rejects it with 400; account without edit rights (403); title already at GitLab's 255-character limit so adding 'Draft: ' overflows validation; expired token (401).","commonSituations":"Toggle races with the user deleting or closing the MR in the web UI; very long MR titles that exceed the limit once the prefix is added; automated 'mark ready' flows running against stale MR state.","solutions":["Inspect error_text in the message — GitLab states which title validation failed","Re-fetch the MR and skip the toggle if it is already in the desired state (title already has/lacks the prefix)","Truncate or reject titles longer than ~245 chars before adding the Draft: prefix","For 404, drop the stale MR from local state; for 403, check the account can edit the MR"],"exampleFix":"// before\nclient.set_merge_request_draft_state(&params).await?;\n\n// after: no-op when already in the desired state, and pre-validate the title length\nlet mr = client.get_merge_request(params.project_id.clone(), params.mr_iid).await?;\nlet wants_draft = params.draft; // true = mark as draft\nlet is_draft = mr.title.starts_with(\"Draft: \");\nif is_draft != wants_draft {\n    ensure!(mr.title.len() + 7 <= 255, \"title too long to toggle draft prefix\");\n    client.set_merge_request_draft_state(&params).await?;\n}","handlingStrategy":"validation","validationCode":"// No-op when already in the desired state; guard title length before prefixing\nlet mr = client.get_merge_request(params.project_id.clone(), params.mr_iid).await?;\nlet is_draft = mr.title.starts_with(\"Draft: \");\nensure!(is_draft != params.draft, \"nothing to toggle\");\nensure!(mr.title.len() + \"Draft: \".len() <= 255, \"title too long for draft prefix\");\nclient.set_merge_request_draft_state(&params).await?;","typeGuard":null,"tryCatchPattern":"if let Err(err) = client.set_merge_request_draft_state(&params).await {\n    if err.to_string().contains(\"404\") { /* MR deleted meanwhile; refresh view */ }\n    else { return Err(err); }\n}","preventionTips":["Never toggle draft state from stale cached titles — always re-fetch first","Enforce a UI-side title length limit below 255 chars so the prefix always fits"],"tags":["gitlab","http","merge-request","draft","title-validation","rust"],"backgroundTag":"gitlab-api-error","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}