{"record":{"id":"01fca6e49b67a763","repo":"gitbutlerapp/gitbutler","slug":"expected-project-id-to-be-either-a-projecthandle","errorCode":null,"errorMessage":"Expected `project_id` to be either a ProjectHandle or a legacy ProjectId, got '{value}'","messagePattern":"Expected `project_id` to be either a ProjectHandle or a legacy ProjectId, got '(.+?)'","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-project-handle/src/project_handle.rs","lineNumber":157,"sourceCode":"    {\n        let value = <String as serde::Deserialize>::deserialize(deserializer)?;\n        value.parse().map_err(serde::de::Error::custom)\n    }\n}\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}","sourceCodeStart":139,"sourceCodeEnd":175,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-project-handle/src/project_handle.rs#L139-L175","documentation":"`ProjectHandleOrLegacyProjectId::from_str` (built with the `legacy` feature) tries to parse the string first as a `ProjectHandle`, then as a numeric `LegacyProjectId`, and throws this when both fail. It is the API-boundary type that accepts either the new handle format or the old numeric project id when routing requests like `project_id` in URLs/JSON. The message names the offending value.","triggerScenarios":"Passing a project_id that is neither a valid ProjectHandle string nor a plain integer, e.g. \"abc\", \"#12\", \"12.0\", or \"\"; also numeric strings with whitespace like \" 12\" when the legacy parse is strict.","commonSituations":"Stale URLs or stored references from before the handle migration that were hand-edited; clients sending the wrong field or quoting numbers as floats; JSON payloads where project_id is null-coerced to an empty string.","solutions":["Send a valid ProjectHandle string (as produced by current GitButler) or a plain numeric id like \"42\" when the legacy feature is enabled","Log the exact offending value and fix the client/URL that produced it","If the value should have been numeric, strip whitespace/quotes before parsing"],"exampleFix":"// before\nlet id: ProjectHandleOrLegacyProjectId = \"project-#1\".parse()?; // Err\n\n// after\nlet id: ProjectHandleOrLegacyProjectId = \"01J9Z8Q9H4V2A5B6C7D8E9F0G1\".parse()?; // handle\n// or, with the legacy feature:\nlet id: ProjectHandleOrLegacyProjectId = \"42\".parse()?; // legacy numeric id","handlingStrategy":"validation","validationCode":"let looks_like_handle = !value.is_empty() && value.chars().all(|c| c.is_ascii_alphanumeric());\nlet looks_like_legacy = value.chars().all(|c| c.is_ascii_digit()) && !value.is_empty();\nanyhow::ensure!(looks_like_handle || looks_like_legacy, \"bad project_id: {value}\");","typeGuard":"fn accepts_project_id(value: &str) -> bool {\n    value.parse::<ProjectHandle>().is_ok()\n        || value.chars().all(|c| c.is_ascii_digit()) && !value.is_empty()\n}","tryCatchPattern":"match value.parse::<ProjectHandleOrLegacyProjectId>() {\n    Ok(id) => id,\n    Err(e) => return Err(e.context(format!(\"project_id {value:?} is neither a handle nor a numeric id\"))),\n}","preventionTips":["Validate project ids at the API boundary before dispatch","Never hand-construct id strings; use values returned by the projects API","Trim whitespace from ids coming from URLs or query params"],"tags":["rust","parsing","identifier","migration","api-boundary"],"backgroundTag":"invalid-identifier-format","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}