gitbutlerapp/gitbutler · error
Expected `project_id` to be either a ProjectHandle or a lega
Error message
Expected `project_id` to be either a ProjectHandle or a legacy ProjectId, got '{value}' What it means
`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.
Source
Thrown at crates/but-project-handle/src/project_handle.rs:157
{
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
value.parse().map_err(serde::de::Error::custom)
}
}
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)
}
}View on GitHub (pinned to caf1f223d3)
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
Example fix
// before let id: ProjectHandleOrLegacyProjectId = "project-#1".parse()?; // Err // after let id: ProjectHandleOrLegacyProjectId = "01J9Z8Q9H4V2A5B6C7D8E9F0G1".parse()?; // handle // or, with the legacy feature: let id: ProjectHandleOrLegacyProjectId = "42".parse()?; // legacy numeric id
Defensive patterns
Strategy: validation
Validate before calling
let looks_like_handle = !value.is_empty() && value.chars().all(|c| c.is_ascii_alphanumeric());
let looks_like_legacy = value.chars().all(|c| c.is_ascii_digit()) && !value.is_empty();
anyhow::ensure!(looks_like_handle || looks_like_legacy, "bad project_id: {value}"); Type guard
fn accepts_project_id(value: &str) -> bool {
value.parse::<ProjectHandle>().is_ok()
|| value.chars().all(|c| c.is_ascii_digit()) && !value.is_empty()
} Try / catch
match value.parse::<ProjectHandleOrLegacyProjectId>() {
Ok(id) => id,
Err(e) => return Err(e.context(format!("project_id {value:?} is neither a handle nor a numeric id"))),
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
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 a ProjectHandle, got '{value}'
- 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
- line {line_number}: merge requires exactly one commit
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/01fca6e49b67a763.
Report an issue: GitHub.