{"record":{"id":"7088f6762f7d2c71","repo":"gitbutlerapp/gitbutler","slug":"stopped-listing-gitlab-merge-requests-after-unsafe","errorCode":null,"errorMessage":"Stopped listing GitLab merge requests after unsafe pagination state","messagePattern":"Stopped listing GitLab merge requests after unsafe pagination state","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-gitlab/src/client.rs","lineNumber":210,"sourceCode":"        );\n\n        self.list_merge_requests(&url, &[], \"Failed to list merge requests for commit\")\n            .await\n    }\n\n    async fn list_merge_requests(\n        &self,\n        url: &str,\n        query: &[(&str, &str)],\n        error_message: &str,\n    ) -> Result<Vec<MergeRequest>> {\n        let mut mrs = Vec::new();\n        let mut next_page = Some(\"1\".to_string());\n        let mut seen_pages = HashSet::new();\n\n        while let Some(page) = next_page.take() {\n            if !merge_request_page_is_safe(&page, &mut seen_pages) {\n                bail!(\"Stopped listing GitLab merge requests after unsafe pagination state\");\n            }\n\n            let response = self\n                .client\n                .get(url)\n                .query(query)\n                .query(&[(\"per_page\", \"100\"), (\"page\", page.as_str())])\n                .send()\n                .await?;\n            if !response.status().is_success() {\n                return Err(anyhow::Error::from(HttpStatusError {\n                    status: response.status(),\n                })\n                .context(error_message.to_owned()));\n            }\n\n            next_page = next_page_from_headers(response.headers());\n            let mut page_mrs: Vec<GitLabMergeRequest> = response.json().await?;","sourceCodeStart":192,"sourceCodeEnd":228,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-gitlab/src/client.rs#L192-L228","documentation":"While paging through GitLab merge requests, the client detected an unsafe pagination state via `merge_request_page_is_safe` — e.g. a repeated page number (already in seen_pages) or a non-numeric/cyclic next-page cursor — and aborts instead of looping forever or duplicating results.","triggerScenarios":"Calling list_merge_requests (used by list_open_mrs, list_mrs_for_target, list_mrs_for_commit) when GitLab's X-Next-Page header repeats a previously visited page or is invalid, typically from a concurrently modified MR list or a proxy mangling pagination headers.","commonSituations":"MRs created/closed during pagination shifting page boundaries; misbehaving reverse proxy stripping or duplicating headers; GitLab paging API changes; huge result sets with unstable sort order.","solutions":["Add a deterministic sort (e.g. sort=created_asc or updated_asc) to the query so pages are stable during iteration.","Re-run the listing — a transient concurrent modification usually resolves on retry.","Check any proxy in front of GitLab for header rewriting (X-Next-Page).","Reduce page size or filter the query to shrink result sets.","If reproducible, verify the GitLab version's pagination header behavior."],"exampleFix":"// before\nlet query = vec![(\"state\", \"opened\"), (\"per_page\", \"100\")];\n\n// after\nlet query = vec![(\"state\", \"opened\"), (\"per_page\", \"100\"), (\"sort\", \"asc\"), (\"order_by\", \"created_at\")];","handlingStrategy":"retry","validationCode":"// ensure a stable sort is part of the pagination query\nanyhow::ensure!(query.iter().any(|(k, _)| *k == \"order_by\"), \"pagination query needs a stable order_by\");","typeGuard":null,"tryCatchPattern":"match client.list_merge_requests(&query).await {\n    Ok(mrs) => mrs,\n    Err(e) if e.to_string().contains(\"unsafe pagination state\") => {\n        tokio::time::sleep(Duration::from_secs(1)).await;\n        client.list_merge_requests(&stable_query).await? // retry with stable sort\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Always sort deterministically (order_by=created_at, sort=asc) when paginating.","Keep page sizes moderate to shorten the pagination window.","Check proxies for GitLab pagination header rewriting (X-Next-Page).","Retry listings that fail mid-pagination due to concurrent changes."],"tags":["gitlab","pagination","api","network"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}