{"record":{"id":"2819cac691ddade8","repo":"neondatabase/neon","slug":"tenanttimelineid-must-contain-timeline-id","errorCode":null,"errorMessage":"TenantTimelineId must contain timeline_id","messagePattern":"TenantTimelineId must contain timeline_id","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"libs/utils/src/id.rs","lineNumber":342,"sourceCode":"\nimpl fmt::Display for TenantTimelineId {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        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    }","sourceCodeStart":324,"sourceCodeEnd":360,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/utils/src/id.rs#L324-L360","documentation":"TenantTimelineId::from_str splits on '/' and requires exactly two components; this variant fires when there is no '/' at all, so only the tenant_id component was supplied. The parser cannot guess a timeline, so it fails with this message.","triggerScenarios":"Parsing a single-component string such as a bare tenant id or timeline id where a '<tenant_id>/<timeline_id>' pair is required (config files, CLI args, HTTP routes).","commonSituations":"Passing only NEON_TIMELINE_ID where a pair is expected; copy-paste that dropped the second half after the slash.","solutions":["Append the missing '/<timeline_id>' to form the two-part id","Source both ids from the console/CLI and join them with '/'","Validate the format at the config boundary with a split-based check"],"exampleFix":"// before\nlet ttid: TenantTimelineId = tenant_id.to_string().parse()?;\n// after\nlet ttid: TenantTimelineId = format!(\"{tenant_id}/{timeline_id}\").parse()?;","handlingStrategy":"type-guard","validationCode":"fn has_both_components(s: &str) -> bool {\n    s.split('/').count() == 2\n}\n\nanyhow::ensure!(has_both_components(s), \"expected '<tenant_id>/<timeline_id>', got '{s}'\");","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 = match s.parse::<TenantTimelineId>() {\n    Ok(id) => id,\n    Err(e) => anyhow::bail!(\"'{s}' is not '<tenant_id>/<timeline_id>': {e}\"),\n};","preventionTips":["Always join tenant and timeline ids with format!(\"{tenant}/{timeline}\")","Reject single-component values in input validation schemas"],"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"}