gitbutlerapp/gitbutler · error · anyhow::Error
Failed to get latest pipeline for ref: {status}
Error message
Failed to get latest pipeline for ref: {status} What it means
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.
Source
Thrown at crates/but-gitlab/src/client.rs:650
let url = format!("{}/projects/{}/pipelines/latest", self.base_url, project_id);
let response = self
.client
.get(&url)
.query(&[("ref", reference)])
.send()
.await
.with_context(|| {
format!("Failed to get latest GitLab pipeline for ref '{reference}'")
})?;
if !response.status().is_success() {
let status = response.status();
if status == reqwest::StatusCode::FORBIDDEN || status == reqwest::StatusCode::NOT_FOUND
{
return Ok(Vec::new());
}
bail!("Failed to get latest pipeline for ref: {status}");
}
let pipeline: GitLabPipelineResponse = response
.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;View on GitHub (pinned to caf1f223d3)
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
Example fix
// before
let jobs = client.list_pipeline_jobs_for_ref(project_id, branch).await?; // one 429 kills the view
// after
let jobs = match client.list_pipeline_jobs_for_ref(project_id, branch).await {
Ok(jobs) => jobs,
Err(err) if err.to_string().contains("429") => {
tokio::time::sleep(Duration::from_secs(30)).await;
client.list_pipeline_jobs_for_ref(project_id, branch).await?
}
Err(err) => return Err(err),
}; Defensive patterns
Strategy: fallback
Try / catch
let jobs = match client.list_pipeline_jobs_for_ref(project_id, branch).await {
Ok(jobs) => jobs,
Err(err) => {
let m = err.to_string();
if m.contains("429") || m.contains("500") || m.contains("502") || m.contains("503") {
tracing::warn!(%branch, "pipeline lookup degraded: {m}");
Vec::new() // show 'unknown CI state' instead of failing the view
} else { return Err(err); }
}
}; Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- Failed to enable merge request auto-merge: {status} - {error
- Failed to list jobs for pipeline: {}
- Failed to get merge request: {}
- Failed to get MR merge status: {}
- Failed to update merge request: {status} - {error_text}
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/437c29f91bcd7736.
Report an issue: GitHub.