{"record":{"id":"437c29f91bcd7736","repo":"gitbutlerapp/gitbutler","slug":"failed-to-get-latest-pipeline-for-ref-status","errorCode":null,"errorMessage":"Failed to get latest pipeline for ref: {status}","messagePattern":"Failed to get latest pipeline for ref: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-gitlab/src/client.rs","lineNumber":650,"sourceCode":"\n        let url = format!(\"{}/projects/{}/pipelines/latest\", self.base_url, project_id);\n        let response = self\n            .client\n            .get(&url)\n            .query(&[(\"ref\", reference)])\n            .send()\n            .await\n            .with_context(|| {\n                format!(\"Failed to get latest GitLab pipeline for ref '{reference}'\")\n            })?;\n\n        if !response.status().is_success() {\n            let status = response.status();\n            if status == reqwest::StatusCode::FORBIDDEN || status == reqwest::StatusCode::NOT_FOUND\n            {\n                return Ok(Vec::new());\n            }\n            bail!(\"Failed to get latest pipeline for ref: {status}\");\n        }\n\n        let pipeline: GitLabPipelineResponse = response\n            .json()\n            .await\n            .with_context(|| format!(\"Failed to parse GitLab pipeline for ref '{reference}'\"))?;\n\n        let pipeline_web_url = pipeline.web_url;\n        let pipeline_status = Some(pipeline.status);\n\n        let jobs_url = format!(\n            \"{}/projects/{}/pipelines/{}/jobs\",\n            self.base_url, project_id, pipeline.id\n        );\n        let mut jobs = Vec::new();\n        let mut next_page = Some(\"1\".to_string());\n        let mut seen_pages = HashSet::new();\n        let mut pages_iterated = 0;","sourceCodeStart":632,"sourceCodeEnd":668,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-gitlab/src/client.rs#L632-L668","documentation":"Thrown by GitLabClient::list_pipeline_jobs_for_ref when GET /projects/:id/pipelines/latest?ref=:ref fails with any status other than 403 and 404 — those two are deliberately converted to Ok(vec![]) because they legitimately mean 'no CI on this instance' or 'no pipeline for that ref'. So seeing this bail means something else: typically 401 (expired token), 429 (rate limit), or 5xx (GitLab incident). Transport errors and JSON parse errors from the same call are separate, context-wrapped errors.","triggerScenarios":"Expired/revoked token (401) while listing CI for a branch; hitting GitLab rate limits (429) when polling pipelines frequently; GitLab 5xx during an incident; passing a ref name that triggers a 400 (empty or malformed ref parameter).","commonSituations":"UI polls pipeline status every few seconds and trips 429 rate limiting; token expired between long sessions; GitLab.com outage; self-hosted instance behind a proxy that returns 502.","solutions":["For 429: back off — honor Retry-After and increase the polling interval","Re-auth with `but config forge auth` on 401","For 5xx, retry after a short delay; check https://status.gitlab.com if on GitLab.com","Note 403/404 are already handled as 'no pipeline' by the client — if you expected a pipeline there, the ref name or CI config is the problem, not this error"],"exampleFix":"// before\nlet jobs = client.list_pipeline_jobs_for_ref(project_id, branch).await?; // one 429 kills the view\n\n// after\nlet jobs = match client.list_pipeline_jobs_for_ref(project_id, branch).await {\n    Ok(jobs) => jobs,\n    Err(err) if err.to_string().contains(\"429\") => {\n        tokio::time::sleep(Duration::from_secs(30)).await;\n        client.list_pipeline_jobs_for_ref(project_id, branch).await?\n    }\n    Err(err) => return Err(err),\n};","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"let jobs = match client.list_pipeline_jobs_for_ref(project_id, branch).await {\n    Ok(jobs) => jobs,\n    Err(err) => {\n        let m = err.to_string();\n        if m.contains(\"429\") || m.contains(\"500\") || m.contains(\"502\") || m.contains(\"503\") {\n            tracing::warn!(%branch, \"pipeline lookup degraded: {m}\");\n            Vec::new() // show 'unknown CI state' instead of failing the view\n        } else { return Err(err); }\n    }\n};","preventionTips":["Space out pipeline polling and honor Retry-After to stay off 429","Remember 403/404 already map to 'no pipeline' inside the client — don't duplicate that handling"],"tags":["gitlab","http","pipeline","rate-limit","rust"],"backgroundTag":"gitlab-api-error","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}