gitbutlerapp/gitbutler · error · anyhow::Error

Stopped listing GitLab jobs for pipeline {} after unsafe pag

Error message

Stopped listing GitLab jobs for pipeline {} after unsafe pagination state

What it means

A loop-safety bail inside list_pipeline_jobs_for_ref's pagination over GET /projects/:id/pipelines/:id/jobs?per_page=100&page=N. It fires when either guard trips: pages_iterated reaches MAX_PIPELINE_JOB_PAGES (25 pages × 100 jobs = 2500 jobs), or the X-NextPage header (read by next_page_from_headers) yields a page string already seen — i.e., the server is cycling, which would loop forever. This is a deliberate fail-fast against GitLab or an intermediary misbehaving, and against unbounded memory on monster pipelines.

Source

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

            .json()
            .await
            .with_context(|| format!("Failed to parse GitLab pipeline for ref '{reference}'"))?;

        let pipeline_web_url = pipeline.web_url;
        let pipeline_status = Some(pipeline.status);

        let jobs_url = format!(
            "{}/projects/{}/pipelines/{}/jobs",
            self.base_url, project_id, pipeline.id
        );
        let mut jobs = Vec::new();
        let mut next_page = Some("1".to_string());
        let mut seen_pages = HashSet::new();
        let mut pages_iterated = 0;

        while let Some(page) = next_page.take() {
            if pages_iterated >= MAX_PIPELINE_JOB_PAGES || !seen_pages.insert(page.clone()) {
                bail!(
                    "Stopped listing GitLab jobs for pipeline {} after unsafe pagination state",
                    pipeline.id
                );
            }
            pages_iterated += 1;

            let response = self
                .client
                .get(&jobs_url)
                .query(&[("per_page", "100"), ("page", page.as_str())])
                .send()
                .await
                .with_context(|| {
                    format!("Failed to list GitLab jobs for pipeline {}", pipeline.id)
                })?;

            if !response.status().is_success() {
                bail!("Failed to list jobs for pipeline: {}", response.status());

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Check the pipeline in GitLab's UI: if it genuinely has >2500 jobs, reduce the job count (matrix fan-out, needs:triggers) or raise MAX_PIPELINE_JOB_PAGES locally with a deliberate decision
  2. If job count is modest, capture the raw response headers of one jobs request — a repeated X-NextPage/Link header points at a proxy or middleware rewriting them; fix or bypass the proxy for /api/v4
  3. Retry once: transient header inconsistency under load usually clears
  4. File the pipeline id and header trace in a but-gitlab issue if GitLab itself cycles pages

Example fix

// before (client-side, nothing to change in the caller's request)
let jobs = client.list_pipeline_jobs_for_ref(project_id, branch).await?;

// after: detect the safety-bail and degrade instead of failing the whole CI view
let jobs = match client.list_pipeline_jobs_for_ref(project_id, branch).await {
    Ok(jobs) => jobs,
    Err(err) if err.to_string().contains("unsafe pagination state") => {
        tracing::warn!(%branch, "pipeline jobs truncated; showing no CI state");
        Vec::new()
    }
    Err(err) => return Err(err),
};
Defensive patterns

Strategy: try-catch

Try / catch

match client.list_pipeline_jobs_for_ref(project_id, branch).await {
    Ok(jobs) => jobs,
    Err(err) if err.to_string().contains("unsafe pagination state") => {
        tracing::error!(%branch, "pipeline jobs listing hit the pagination guard (see pipeline in web UI)");
        Vec::new()
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: A pipeline with more than 2500 jobs (huge matrix builds, tool-generated jobs) exhausts the 25-page cap; a caching proxy or middleware rewrites/drops pagination headers so X-NextPage keeps naming a page already fetched; GitLab returns inconsistent next-page headers under load.

Common situations: Generated matrix pipelines (thousands of jobs) in large repos; self-hosted GitLab behind nginx/CDN that mangles Link/X-NextPage headers; API smoke tests against mocked responses that always return the same next page.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/7321b04bd13a406a. Report an issue: GitHub.