{"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":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-gitlab/src/client.rs","lineNumber":198,"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                bail!(\"{error_message}: {}\", response.status());\n            }\n\n            next_page = next_page_from_headers(response.headers());\n            let mut page_mrs: Vec<GitLabMergeRequest> = response.json().await?;\n            if page_mrs.is_empty() {\n                break;\n            }","sourceCodeStart":180,"sourceCodeEnd":216,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-gitlab/src/client.rs#L180-L216","documentation":"list_merge_requests follows GitLab's x-next-page header page by page (per_page=100). Before each request it validates the page number: it must parse as a positive integer, must not have been visited already, and the total number of fetched pages must stay under 10,000 (MAX_MERGE_REQUEST_REQUESTS). When the server's pagination headers violate these rules - typically by replaying the same next page forever - the loop would never terminate, so the client stops and fails instead.","triggerScenarios":"A GitLab instance or a proxy in front of it returns a duplicate x-next-page value so the same page repeats; a non-numeric or zero next-page header; a listing so vast it would exceed 10,000 pages of 100 merge requests.","commonSituations":"Self-managed GitLab versions with pagination bugs; reverse proxies that mangle response headers; extremely large projects.","solutions":["Retry the operation - header anomalies are often transient","If reproducible, capture the x-next-page headers (curl -I) and check the GitLab version and any proxy configuration","Narrow the listing with filters (state, assignee, updated_after) to reduce the page count"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"let mrs = match client.list_merge_requests(...).await {\n    Err(e) if e.to_string().contains(\"unsafe pagination state\") => {\n        tokio::time::sleep(Duration::from_secs(5)).await;\n        client.list_merge_requests(...).await? // one retry: header glitches are usually transient\n    }\n    other => other?,\n};","preventionTips":["Add filters (state, assignee, updated_after) so listings fit few pages","Keep GitLab patched - pagination header bugs are version-specific","Check proxies for header mangling when this repeats"],"tags":["gitlab","pagination","api","loop-protection"],"backgroundTag":"pagination-loop-protection","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}