{"record":{"id":"310750cfe42a298e","repo":"gitbutlerapp/gitbutler","slug":"failed-to-list-jobs-for-pipeline","errorCode":null,"errorMessage":"Failed to list jobs for pipeline: {}","messagePattern":"Failed to list jobs for pipeline: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-gitlab/src/client.rs","lineNumber":690,"sourceCode":"                bail!(\n                    \"Stopped listing GitLab jobs for pipeline {} after unsafe pagination state\",\n                    pipeline.id\n                );\n            }\n            pages_iterated += 1;\n\n            let response = self\n                .client\n                .get(&jobs_url)\n                .query(&[(\"per_page\", \"100\"), (\"page\", page.as_str())])\n                .send()\n                .await\n                .with_context(|| {\n                    format!(\"Failed to list GitLab jobs for pipeline {}\", pipeline.id)\n                })?;\n\n            if !response.status().is_success() {\n                bail!(\"Failed to list jobs for pipeline: {}\", response.status());\n            }\n\n            next_page = next_page_from_headers(response.headers());\n            let mut page_jobs: Vec<GitLabPipelineJob> =\n                response.json().await.with_context(|| {\n                    format!(\"Failed to parse GitLab jobs for pipeline {}\", pipeline.id)\n                })?;\n            if page_jobs.is_empty() {\n                break;\n            }\n            jobs.append(&mut page_jobs);\n        }\n\n        let jobs = normalize_pipeline_jobs(\n            jobs,\n            pipeline_web_url,\n            pipeline_status,\n            &self.base_url,","sourceCodeStart":672,"sourceCodeEnd":708,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-gitlab/src/client.rs#L672-L708","documentation":"Thrown inside list_pipeline_jobs_for_ref when one page request of GET /projects/:id/pipelines/:id/jobs (per_page=100&page=N) returns a non-2xx status after the initial pipeline lookup already succeeded. Distinct from the context-wrapped transport error ('Failed to list GitLab jobs for pipeline {id}') which fires if the request never completes — this bail means GitLab answered and refused. Because it happens mid-pagination, all jobs accumulated so far are discarded.","triggerScenarios":"Page 3+ of the jobs listing returns 429 (rate limit) after pages 1-2 succeeded; token revoked between the pipeline fetch and the jobs fetch (401); a 5xx mid-listing; permissions changed mid-walk so later pages 403.","commonSituations":"Deep pagination against rate limits when several large pipelines are polled concurrently; long-running token expiry hitting mid-request-sequence; GitLab rolling restarts during incidents.","solutions":["For 429: back off (honor Retry-After) and restart the listing — this client does not cache partial pages","For 401: re-run `but config forge auth` and retry","For 5xx: retry with exponential backoff; check GitLab status","Reduce polling frequency of large pipelines to stay under rate limits"],"exampleFix":"// before\nlet jobs = client.list_pipeline_jobs_for_ref(project_id, branch).await?;\n\n// after: retry once after backoff for transient mid-pagination failures\nlet jobs = match client.list_pipeline_jobs_for_ref(project_id.clone(), branch).await {\n    Ok(jobs) => jobs,\n    Err(err) => {\n        let s = err.to_string();\n        if s.contains(\"429\") || s.contains(\"500\") || s.contains(\"502\") || s.contains(\"503\") {\n            tokio::time::sleep(Duration::from_secs(5)).await;\n            client.list_pipeline_jobs_for_ref(project_id, branch).await?\n        } else {\n            return Err(err);\n        }\n    }\n};","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"async fn list_jobs_with_retry(client: &GitLabClient, project_id: GitLabProjectId, branch: &str) -> Result<Vec<GitLabPipelineJob>> {\n    match client.list_pipeline_jobs_for_ref(project_id.clone(), branch).await {\n        Ok(jobs) => Ok(jobs),\n        Err(err) => {\n            let m = err.to_string();\n            if m.contains(\"429\") || m.contains(\"50\") { // transient: retry once after backoff\n                tokio::time::sleep(std::time::Duration::from_secs(5)).await;\n                client.list_pipeline_jobs_for_ref(project_id, branch).await\n            } else { Err(err) }\n        }\n    }\n}","preventionTips":["Rate-limit concurrent pipeline-job listings; large pipelines multiplied by polling easily trip 429","Retry the whole listing (not per page) — the client discards partial pages on failure"],"tags":["gitlab","http","pagination","pipeline","jobs","rust"],"backgroundTag":"gitlab-api-error","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}