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

  1. For 429: back off — honor Retry-After and increase the polling interval
  2. Re-auth with `but config forge auth` on 401
  3. For 5xx, retry after a short delay; check https://status.gitlab.com if on GitLab.com
  4. 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

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


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