{"record":{"id":"49c77a6e04c36dd0","repo":"gitbutlerapp/gitbutler","slug":"expected-project-id-to-be-a-projecthandle-got","errorCode":null,"errorMessage":"Expected `project_id` to be a ProjectHandle, got '{value}'","messagePattern":"Expected `project_id` to be a ProjectHandle, got '(.+?)'","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-project-handle/src/project_handle.rs","lineNumber":161,"sourceCode":"}\n\nimpl std::str::FromStr for ProjectHandleOrLegacyProjectId {\n    type Err = anyhow::Error;\n\n    fn from_str(value: &str) -> Result<Self, Self::Err> {\n        if let Ok(handle) = value.parse::<ProjectHandle>() {\n            return Ok(Self::ProjectHandle(handle));\n        }\n        #[cfg(feature = \"legacy\")]\n        if let Ok(project_id) = value.parse::<LegacyProjectId>() {\n            return Ok(Self::LegacyProjectId(project_id));\n        }\n        #[cfg(feature = \"legacy\")]\n        return Err(anyhow::anyhow!(\n            \"Expected `project_id` to be either a ProjectHandle or a legacy ProjectId, got '{value}'\"\n        ));\n        #[cfg(not(feature = \"legacy\"))]\n        return Err(anyhow::anyhow!(\n            \"Expected `project_id` to be a ProjectHandle, got '{value}'\"\n        ));\n    }\n}\n\nimpl<'de> serde::Deserialize<'de> for ProjectHandleOrLegacyProjectId {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: serde::Deserializer<'de>,\n    {\n        let value = <String as serde::Deserialize>::deserialize(deserializer)?;\n        value.parse().map_err(serde::de::Error::custom)\n    }\n}\n\nimpl serde::Serialize for ProjectHandleOrLegacyProjectId {\n    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-project-handle/src/project_handle.rs#L143-L179","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (built without `legacy`)\nlet id: ProjectHandleOrLegacyProjectId = \"42\".parse()?; // Err: expected ProjectHandle\n\n// after (translate legacy ids at the boundary)\nlet id = if let Ok(n) = value.parse::<u64>() {\n    lookup_handle_for_legacy_id(n)? // migration map\n} else {\n    value.parse::<ProjectHandleOrLegacyProjectId>()?\n};","handlingStrategy":"validation","validationCode":"// Without the `legacy` feature only handles parse:\nif value.chars().all(|c| c.is_ascii_digit()) {\n    return Err(anyhow::anyhow!(\n        \"legacy numeric project id {value} no longer accepted; migrate to a ProjectHandle\"\n    ));\n}\nlet id = value.parse::<ProjectHandle>()?;","typeGuard":"fn is_handle_string(value: &str) -> bool {\n    value.parse::<ProjectHandle>().is_ok()\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["rust","parsing","identifier","migration","feature-flags"],"backgroundTag":"invalid-identifier-format","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}