{"record":{"id":"41def527890e5a2c","repo":"gitbutlerapp/gitbutler","slug":"failed-to-update-merge-request-status-error","errorCode":null,"errorMessage":"Failed to update merge request: {status} - {error_text}","messagePattern":"Failed to update merge request: (.+?) - (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-gitlab/src/client.rs","lineNumber":402,"sourceCode":"                .await?;\n            Some(update_draft_state_in_title(title, mr.draft))\n        } else {\n            None\n        };\n\n        let body = UpdateMergeRequestBody {\n            title: title.as_deref(),\n            description: params.description,\n            target_branch: params.target_branch,\n            state_event: params.state_event,\n        };\n\n        let response = self.client.put(&url).json(&body).send().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 update merge request: {status} - {error_text}\");\n        }\n\n        let mr: GitLabMergeRequest = response.json().await?;\n        Ok(mr.into())\n    }\n\n    pub async fn merge_merge_request(&self, params: &MergeMergeRequestParams) -> Result<()> {\n        #[derive(Serialize)]\n        struct MergeMergeRequestBody {\n            #[serde(skip_serializing_if = \"Option::is_none\")]\n            squash: Option<bool>,\n        }\n\n        let url = format!(\n            \"{}/projects/{}/merge_requests/{}/merge\",\n            self.base_url, params.project_id, params.mr_iid\n        );\n","sourceCodeStart":384,"sourceCodeEnd":420,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-gitlab/src/client.rs#L384-L420","documentation":"Thrown by GitLabClient::update_merge_request when the PUT to /projects/:id/merge_requests/:iid fails. Unlike most bails in this client it also forwards the response body (error_text), which contains GitLab's validation message, e.g. `{\"message\":\"Target branch does not exist\"}` — read it before doing anything else. The body updates title, description, target_branch and state_event (close/reopen) in one call.","triggerScenarios":"PUT with target_branch set to a branch that no longer exists in the target project; state_event close/reopen on an MR in a conflicting state; title/description rejected by validation (e.g., empty title); 403 when the account lacks Developer rights to edit the MR; editing an MR that was just deleted (404).","commonSituations":"Retargeting an MR to a branch that was renamed or deleted by CI; automated title/description updates racing with the user closing the MR; permissions downgraded after the MR was opened; updating an MR on a fork whose upstream target moved.","solutions":["Read the {error_text} portion of the message — GitLab names the exact invalid field","If target_branch is rejected, verify the branch exists (GET /projects/:id/repository/branches/:name) and re-run with the correct name","Re-fetch the MR (get_merge_request) to confirm it still exists and is not already in the requested state before re-sending","For 403, check the account's role on the project (Developer+ to edit MRs)","Retry once after `but config forge auth` if 401"],"exampleFix":"// before\nclient.update_merge_request(&params).await?;\n\n// after: guard the two common validation failures before the PUT\nlet mr = client.get_merge_request(project_id.clone(), mr_iid).await?;\nif let Some(target) = params.target_branch {\n    ensure!(target.is_empty().not(), \"target branch name must not be empty\");\n    ensure!(mr.state == \"opened\", \"cannot update a {} MR\", mr.state);\n}\nclient.update_merge_request(&params).await?;","handlingStrategy":"validation","validationCode":"// Reject the two common invalid payloads before the PUT\nif let Some(branch) = params.target_branch {\n    ensure!(!branch.trim().is_empty(), \"target branch must not be empty\");\n}\nensure!(matches!(params.state_event, None | Some(\"close\") | Some(\"reopen\")), \"invalid state_event\");\nclient.update_merge_request(&params).await?;","typeGuard":null,"tryCatchPattern":"if let Err(err) = client.update_merge_request(&params).await {\n    let msg = err.to_string();\n    if msg.contains(\"Target branch\") { /* re-fetch branches, prompt user to pick again */ }\n    else { return Err(err); }\n}","preventionTips":["Always read the {error_text} segment — GitLab names the offending field","Re-fetch the MR before mutating to avoid stale-state updates (409-ish races)"],"tags":["gitlab","http","merge-request","validation","rust"],"backgroundTag":"gitlab-api-error","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}