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>
    where

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Migrate stored numeric project ids to ProjectHandles before deploying a build without the legacy feature
  2. Re-enable the `legacy` cargo feature on but-project-handle during a transition period
  3. 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

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


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