gitbutlerapp/gitbutler · error

Stopped listing GitLab merge requests after unsafe…

Error message

Stopped listing GitLab merge requests after unsafe pagination state

What it means

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.

Solutions

  1. Add a deterministic sort (e.g. sort=created_asc or updated_asc) to the query so pages are stable during iteration.
  2. Re-run the listing — a transient concurrent modification usually resolves on retry.
  3. Check any proxy in front of GitLab for header rewriting (X-Next-Page).
  4. Reduce page size or filter the query to shrink result sets.
  5. If reproducible, verify the GitLab version's pagination header behavior.

Example fix

// before
let query = vec![("state", "opened"), ("per_page", "100")];

// after
let query = vec![("state", "opened"), ("per_page", "100"), ("sort", "asc"), ("order_by", "created_at")];
Defensive patterns

Strategy: retry

Validate before calling

// ensure a stable sort is part of the pagination query
anyhow::ensure!(query.iter().any(|(k, _)| *k == "order_by"), "pagination query needs a stable order_by");

Try / catch

match client.list_merge_requests(&query).await {
    Ok(mrs) => mrs,
    Err(e) if e.to_string().contains("unsafe pagination state") => {
        tokio::time::sleep(Duration::from_secs(1)).await;
        client.list_merge_requests(&stable_query).await? // retry with stable sort
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/7088f6762f7d2c71. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-gitlab/src/client.rs:210

        );

        self.list_merge_requests(&url, &[], "Failed to list merge requests for commit")
            .await
    }

    async fn list_merge_requests(
        &self,
        url: &str,
        query: &[(&str, &str)],
        error_message: &str,
    ) -> Result<Vec<MergeRequest>> {
        let mut mrs = Vec::new();
        let mut next_page = Some("1".to_string());
        let mut seen_pages = HashSet::new();

        while let Some(page) = next_page.take() {
            if !merge_request_page_is_safe(&page, &mut seen_pages) {
                bail!("Stopped listing GitLab merge requests after unsafe pagination state");
            }

            let response = self
                .client
                .get(url)
                .query(query)
                .query(&[("per_page", "100"), ("page", page.as_str())])
                .send()
                .await?;
            if !response.status().is_success() {
                return Err(anyhow::Error::from(HttpStatusError {
                    status: response.status(),
                })
                .context(error_message.to_owned()));
            }

            next_page = next_page_from_headers(response.headers());
            let mut page_mrs: Vec<GitLabMergeRequest> = response.json().await?;

View on GitHub (pinned to 58e5313667)