{"record":{"id":"3b80e55e3d7f986d","repo":"neondatabase/neon","slug":"tenanttimelineid-must-contain-only-tenant-id-and-t","errorCode":null,"errorMessage":"TenantTimelineId must contain only tenant_id and timeline_id","messagePattern":"TenantTimelineId must contain only tenant_id and timeline_id","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"libs/utils/src/id.rs","lineNumber":345,"sourceCode":"        write!(f, \"{}/{}\", self.tenant_id, self.timeline_id)\n    }\n}\n\nimpl FromStr for TenantTimelineId {\n    type Err = anyhow::Error;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        let mut parts = s.split('/');\n        let tenant_id = parts\n            .next()\n            .ok_or_else(|| anyhow::anyhow!(\"TenantTimelineId must contain tenant_id\"))?\n            .parse()?;\n        let timeline_id = parts\n            .next()\n            .ok_or_else(|| anyhow::anyhow!(\"TenantTimelineId must contain timeline_id\"))?\n            .parse()?;\n        if parts.next().is_some() {\n            anyhow::bail!(\"TenantTimelineId must contain only tenant_id and timeline_id\");\n        }\n        Ok(TenantTimelineId::new(tenant_id, timeline_id))\n    }\n}\n\n// Unique ID of a storage node (safekeeper or pageserver). Supposed to be issued\n// by the console.\n#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd, Hash, Debug, Serialize, Deserialize)]\n#[serde(transparent)]\npub struct NodeId(pub u64);\n\nimpl fmt::Display for NodeId {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"{}\", self.0)\n    }\n}\n\nimpl FromStr for NodeId {","sourceCodeStart":327,"sourceCodeEnd":363,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/utils/src/id.rs#L327-L363","documentation":"TenantTimelineId::from_str requires exactly two '/'-separated components; if a third component exists (parts.next() returns Some), parsing fails. Common shapes include 'a/b/c' and the easy-to-miss trailing slash 'a/b/', which yields an empty third component.","triggerScenarios":"Parsing an id string with more than one '/', e.g. 'tenant/timeline/' (trailing slash from shell completion or string joins), 'tenant/timeline/extra', or URLs accidentally passed whole.","commonSituations":"Trailing slashes appended by path-joining helpers; passing a URL path fragment instead of the bare pair; string concatenation bugs adding extra separators.","solutions":["Strip trailing slashes before parsing: s.trim_end_matches('/')","Check for accidental inclusion of a path prefix or suffix in the string","Join components explicitly with exactly one '/'"],"exampleFix":"// before\nlet ttid: TenantTimelineId = format!(\"{tenant_id}/{timeline_id}/\").parse()?;\n// after\nlet ttid: TenantTimelineId = format!(\"{tenant_id}/{timeline_id}\").parse()?;","handlingStrategy":"type-guard","validationCode":"fn has_exactly_two_components(s: &str) -> bool {\n    let trimmed = s.trim_end_matches('/');\n    trimmed.split('/').count() == 2 && !trimmed.ends_with('/')\n}\n\nanyhow::ensure!(has_exactly_two_components(s), \"expected exactly '<tenant_id>/<timeline_id>'\");","typeGuard":"fn parse_tenant_timeline_id(s: &str) -> Option<TenantTimelineId> {\n    let (t, r) = s.split_once('/')?;\n    if r.contains('/') || t.is_empty() || r.is_empty() {\n        return None;\n    }\n    TenantTimelineId::new(t.parse().ok()?, r.parse().ok()?)\n}","tryCatchPattern":"let ttid = s.trim_end_matches('/').parse::<TenantTimelineId>().map_err(|e| {\n    anyhow::anyhow!(\"'{s}' must be exactly '<tenant_id>/<timeline_id>': {e}\")\n})?;","preventionTips":["Trim trailing slashes before parsing","Never pass URL paths where a bare id pair is expected","Use split_once-based construction instead of string concatenation"],"tags":["rust","parsing","ids","config"],"backgroundTag":"invalid-id-format","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}