gitbutlerapp/gitbutler · error
Expected `project_id` to be a ProjectHandle, got '{value}'
Error message
Expected `project_id` to be a ProjectHandle, got '{value}' What it means
Same `ProjectHandleOrLegacyProjectId::from_str`, but compiled WITHOUT the `legacy` feature: only `ProjectHandle` parsing is attempted, and numeric legacy ids are rejected with this narrower message. Code that used to accept "42" stops working once the crate is built without `legacy`. This is the post-migration error surface.
Source
Thrown at crates/but-project-handle/src/project_handle.rs:161
}
impl std::str::FromStr for ProjectHandleOrLegacyProjectId {
type Err = anyhow::Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if let Ok(handle) = value.parse::<ProjectHandle>() {
return Ok(Self::ProjectHandle(handle));
}
#[cfg(feature = "legacy")]
if let Ok(project_id) = value.parse::<LegacyProjectId>() {
return Ok(Self::LegacyProjectId(project_id));
}
#[cfg(feature = "legacy")]
return Err(anyhow::anyhow!(
"Expected `project_id` to be either a ProjectHandle or a legacy ProjectId, got '{value}'"
));
#[cfg(not(feature = "legacy"))]
return Err(anyhow::anyhow!(
"Expected `project_id` to be a ProjectHandle, got '{value}'"
));
}
}
impl<'de> serde::Deserialize<'de> for ProjectHandleOrLegacyProjectId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
value.parse().map_err(serde::de::Error::custom)
}
}
impl serde::Serialize for ProjectHandleOrLegacyProjectId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
whereView on GitHub (pinned to caf1f223d3)
Solutions
- Migrate stored numeric project ids to ProjectHandles before deploying a build without the legacy feature
- Re-enable the `legacy` cargo feature on but-project-handle during a transition period
- At the API boundary, detect numeric strings and map them through a migration table instead of parsing directly
Example fix
// before (built without `legacy`)
let id: ProjectHandleOrLegacyProjectId = "42".parse()?; // Err: expected ProjectHandle
// after (translate legacy ids at the boundary)
let id = if let Ok(n) = value.parse::<u64>() {
lookup_handle_for_legacy_id(n)? // migration map
} else {
value.parse::<ProjectHandleOrLegacyProjectId>()?
}; Defensive patterns
Strategy: validation
Validate before calling
// Without the `legacy` feature only handles parse:
if value.chars().all(|c| c.is_ascii_digit()) {
return Err(anyhow::anyhow!(
"legacy numeric project id {value} no longer accepted; migrate to a ProjectHandle"
));
}
let id = value.parse::<ProjectHandle>()?; Type guard
fn is_handle_string(value: &str) -> bool {
value.parse::<ProjectHandle>().is_ok()
} Prevention
- Finish numeric-id -> handle migration before building without the legacy feature
- Keep an explicit translation table for ids stored in user data
- Log which parse arm failed so post-migration support tickets are diagnosable
When it happens
Trigger: Building `but-project-handle` without the `legacy` feature and parsing a numeric project id ("42") or any non-handle string; happens after a migration removes the legacy feature from the build.
Common situations: Version upgrades where the legacy compatibility feature was disabled; mixed-version deployments (old client sends numeric ids to a new server); tests written against the legacy behavior.
Understand the failure class
Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.
Related errors
- Expected `project_id` to be either a ProjectHandle or a lega
- implement list and call recursively
- line {line_number}: pick does not accept a message clause
- line {line_number}: pick requires exactly one commit
- line {line_number}: merge does not accept a message clause
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/49c77a6e04c36dd0.
Report an issue: GitHub.