{"record":{"id":"7cb35801532eb428","repo":"neondatabase/neon","slug":"tenanttimelineid-must-contain-tenant-id","errorCode":null,"errorMessage":"TenantTimelineId must contain tenant_id","messagePattern":"TenantTimelineId must contain tenant_id","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"libs/utils/src/id.rs","lineNumber":338,"sourceCode":"    pub fn empty() -> Self {\n        Self::new(TenantId::from([0u8; 16]), TimelineId::from([0u8; 16]))\n    }\n}\n\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","sourceCodeStart":320,"sourceCodeEnd":356,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/utils/src/id.rs#L320-L356","documentation":"TenantTimelineId::from_str expects '<tenant_id>/<timeline_id>'. This variant fires when the tenant_id component before the first '/' is absent. Note that Rust's split always yields at least one (possibly empty) piece, so an empty string actually surfaces as a TenantId parse error; this guard mainly documents the expected two-part shape.","triggerScenarios":"Parsing a malformed identifier from a CLI arg, env var, or HTTP path where the tenant part before '/' is missing, e.g. an empty string or a value beginning with '/'.","commonSituations":"Hand-written scripts exporting truncated tenant/timeline env vars; templated URLs where the tenant placeholder expanded to nothing.","solutions":["Use the canonical '<tenant_id>/<timeline_id>' form with both 32-char hex ids","Copy the full pair from console/neon CLI output instead of typing it","Trim whitespace and newlines from env-provided ids before parsing"],"exampleFix":"// before: missing tenant part\nlet ttid: TenantTimelineId = \"/1f0f4e2c\".parse()?;\n// after\nlet ttid: TenantTimelineId = \"6c0b1b4a7d\".to_owned() + \"/1f0f4e2c\"; // full 32-hex ids","handlingStrategy":"type-guard","validationCode":"fn is_wellformed_tenant_timeline(s: &str) -> bool {\n    let mut parts = s.split('/');\n    parts.next().is_some_and(|t| !t.is_empty())\n        && parts.next().is_some_and(|t| !t.is_empty())\n        && parts.next().is_none()\n}\n\n// before parsing user input\nanyhow::ensure!(is_wellformed_tenant_timeline(s), \"expected '<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.parse::<TenantTimelineId>().map_err(|e| {\n    anyhow::anyhow!(\"invalid TenantTimelineId '{s}', expected '<tenant_id>/<timeline_id>': {e}\")\n})?;","preventionTips":["Validate id strings at the config/API boundary before FromStr","Generate ids programmatically instead of hand-typing them","Trim whitespace and newlines from env-provided values"],"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"}