gitbutlerapp/gitbutler · error · anyhow::Error

Failed to fetch project: {status} - {error_text}

Error message

Failed to fetch project: {status} - {error_text}

What it means

Thrown by GitLabClient::fetch_project_by_path (the impl behind fetch_project and fetch_project_by_numeric_id) when GET /projects/:id-or-path returns non-2xx. The message includes GitLab's response body, which for 404 says exactly what was not found. The function also computes the caller's effective access level from project/group permissions on success, so a failure here also blocks MR enrichment (enrich_merge_request_source_project calls it for the MR's source project).

Source

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

        struct GitLabApiPermissions {
            #[serde(default)]
            project_access: Option<GitLabApiAccess>,
            #[serde(default)]
            group_access: Option<GitLabApiAccess>,
        }

        #[derive(Deserialize)]
        struct GitLabApiAccess {
            access_level: i64,
        }

        let url = format!("{}/projects/{}", self.base_url, project_id);
        let response = self.client.get(&url).send().await?;

        if !response.status().is_success() {
            let status = response.status();
            let error_text = response.text().await.unwrap_or_default();
            bail!("Failed to fetch project: {status} - {error_text}");
        }

        let project: GitLabApiProject = response.json().await?;

        let access_level = project.permissions.and_then(|p| {
            let project_level = p.project_access.map(|a| a.access_level);
            let group_level = p.group_access.map(|a| a.access_level);
            match (project_level, group_level) {
                (Some(a), Some(b)) => Some(a.max(b)),
                (Some(a), None) | (None, Some(a)) => Some(a),
                (None, None) => None,
            }
        });

        Ok(GitLabProject {
            id: project.id,
            path_with_namespace: project.path_with_namespace,
            ssh_url_to_repo: project.ssh_url_to_repo,

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Confirm the project exists at https://<host>/<group>/<project> and copy the exact path (including subgroups)
  2. Read error_text: GitLab distinguishes `404 Project Not Found` from auth issues
  3. If a path with subgroups, pass it verbatim (group/sub/proj); numeric IDs are safer — take `id` from the project page
  4. Check the account still belongs to the project's group; re-run `but config forge auth` for 401
  5. If the project was transferred/renamed, update the stored project id in workspace metadata

Example fix

// before
let project = client.fetch_project(project_id).await?; // wrong-namespace path aborts everything

// after
let project = match client.fetch_project(project_id).await {
    Ok(p) => p,
    Err(err) if err.to_string().contains("404") => {
        anyhow::bail!("project {project_id} not visible to this GitLab account — check namespace or access")
    }
    Err(err) => return Err(err),
};
Defensive patterns

Strategy: validation

Validate before calling

// Sanity-check the identifier shape before hitting the API
fn plausible_project_id(s: &str) -> bool {
    !s.is_empty()
        && (s.chars().all(|c| c.is_ascii_digit()) // numeric id
            || !s.starts_with('/') && !s.ends_with('/') && !s.contains(' ') && !s.ends_with(".git"))
}
ensure!(plausible_project_id(project_id.as_str()), "malformed project id");
client.fetch_project(project_id).await?;

Try / catch

match client.fetch_project(project_id).await {
    Ok(p) => Ok(p),
    Err(err) if err.to_string().contains("404") => bail!("project '{project_id}' not visible to this account"),
    Err(err) => Err(err),
}

Prevention

When it happens

Trigger: Project path with wrong namespace (`group/proj` typo → 404); numeric ID from a different GitLab instance (404); token lacks guest access so the project is invisible (404, GitLab hides forbidden projects); path not URL-encoded for slashes when the client does not encode it; archived/deleted project; expired token (401).

Common situations: Repo remote URL parsed into group/project incorrectly (extra `.git`, subgroups dropped); workspace config stores an old project ID after the project was transferred between groups; user removed from the group so every project fetch starts 404ing; fork's source project deleted.

Related errors


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