{"record":{"id":"f6f6455f6ecd6f21","repo":"gitbutlerapp/gitbutler","slug":"failed-to-fetch-project-status-error-text","errorCode":null,"errorMessage":"Failed to fetch project: {status} - {error_text}","messagePattern":"Failed to fetch project: (.+?) - (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-gitlab/src/client.rs","lineNumber":570,"sourceCode":"        struct GitLabApiPermissions {\n            #[serde(default)]\n            project_access: Option<GitLabApiAccess>,\n            #[serde(default)]\n            group_access: Option<GitLabApiAccess>,\n        }\n\n        #[derive(Deserialize)]\n        struct GitLabApiAccess {\n            access_level: i64,\n        }\n\n        let url = format!(\"{}/projects/{}\", self.base_url, project_id);\n        let response = self.client.get(&url).send().await?;\n\n        if !response.status().is_success() {\n            let status = response.status();\n            let error_text = response.text().await.unwrap_or_default();\n            bail!(\"Failed to fetch project: {status} - {error_text}\");\n        }\n\n        let project: GitLabApiProject = response.json().await?;\n\n        let access_level = project.permissions.and_then(|p| {\n            let project_level = p.project_access.map(|a| a.access_level);\n            let group_level = p.group_access.map(|a| a.access_level);\n            match (project_level, group_level) {\n                (Some(a), Some(b)) => Some(a.max(b)),\n                (Some(a), None) | (None, Some(a)) => Some(a),\n                (None, None) => None,\n            }\n        });\n\n        Ok(GitLabProject {\n            id: project.id,\n            path_with_namespace: project.path_with_namespace,\n            ssh_url_to_repo: project.ssh_url_to_repo,","sourceCodeStart":552,"sourceCodeEnd":588,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-gitlab/src/client.rs#L552-L588","documentation":"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).","triggerScenarios":"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).","commonSituations":"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.","solutions":["Confirm the project exists at https://<host>/<group>/<project> and copy the exact path (including subgroups)","Read error_text: GitLab distinguishes `404 Project Not Found` from auth issues","If a path with subgroups, pass it verbatim (group/sub/proj); numeric IDs are safer — take `id` from the project page","Check the account still belongs to the project's group; re-run `but config forge auth` for 401","If the project was transferred/renamed, update the stored project id in workspace metadata"],"exampleFix":"// before\nlet project = client.fetch_project(project_id).await?; // wrong-namespace path aborts everything\n\n// after\nlet project = match client.fetch_project(project_id).await {\n    Ok(p) => p,\n    Err(err) if err.to_string().contains(\"404\") => {\n        anyhow::bail!(\"project {project_id} not visible to this GitLab account — check namespace or access\")\n    }\n    Err(err) => return Err(err),\n};","handlingStrategy":"validation","validationCode":"// Sanity-check the identifier shape before hitting the API\nfn plausible_project_id(s: &str) -> bool {\n    !s.is_empty()\n        && (s.chars().all(|c| c.is_ascii_digit()) // numeric id\n            || !s.starts_with('/') && !s.ends_with('/') && !s.contains(' ') && !s.ends_with(\".git\"))\n}\nensure!(plausible_project_id(project_id.as_str()), \"malformed project id\");\nclient.fetch_project(project_id).await?;","typeGuard":null,"tryCatchPattern":"match client.fetch_project(project_id).await {\n    Ok(p) => Ok(p),\n    Err(err) if err.to_string().contains(\"404\") => bail!(\"project '{project_id}' not visible to this account\"),\n    Err(err) => Err(err),\n}","preventionTips":["Store numeric project ids (from the project page) instead of paths; they survive renames and transfers","Keep the remote-URL parser's output free of '.git' suffixes and stray slashes"],"tags":["gitlab","http","project","namespace","rust"],"backgroundTag":"gitlab-api-error","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}