neondatabase/neon · error
TenantTimelineId must contain tenant_id
Error message
TenantTimelineId must contain tenant_id
What it means
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.
Source
Thrown at libs/utils/src/id.rs:338
pub fn empty() -> Self {
Self::new(TenantId::from([0u8; 16]), TimelineId::from([0u8; 16]))
}
}
impl fmt::Display for TenantTimelineId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}/{}", self.tenant_id, self.timeline_id)
}
}
impl FromStr for TenantTimelineId {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split('/');
let tenant_id = parts
.next()
.ok_or_else(|| anyhow::anyhow!("TenantTimelineId must contain tenant_id"))?
.parse()?;
let timeline_id = parts
.next()
.ok_or_else(|| anyhow::anyhow!("TenantTimelineId must contain timeline_id"))?
.parse()?;
if parts.next().is_some() {
anyhow::bail!("TenantTimelineId must contain only tenant_id and timeline_id");
}
Ok(TenantTimelineId::new(tenant_id, timeline_id))
}
}
// Unique ID of a storage node (safekeeper or pageserver). Supposed to be issued
// by the console.
#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd, Hash, Debug, Serialize, Deserialize)]
#[serde(transparent)]
pub struct NodeId(pub u64);
View on GitHub (pinned to 8f60b04da4)
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
Example fix
// before: missing tenant part let ttid: TenantTimelineId = "/1f0f4e2c".parse()?; // after let ttid: TenantTimelineId = "6c0b1b4a7d".to_owned() + "/1f0f4e2c"; // full 32-hex ids
Defensive patterns
Strategy: type-guard
Validate before calling
fn is_wellformed_tenant_timeline(s: &str) -> bool {
let mut parts = s.split('/');
parts.next().is_some_and(|t| !t.is_empty())
&& parts.next().is_some_and(|t| !t.is_empty())
&& parts.next().is_none()
}
// before parsing user input
anyhow::ensure!(is_wellformed_tenant_timeline(s), "expected '<tenant_id>/<timeline_id>'"); Type guard
fn parse_tenant_timeline_id(s: &str) -> Option<TenantTimelineId> {
let (t, r) = s.split_once('/')?;
if r.contains('/') || t.is_empty() || r.is_empty() {
return None;
}
TenantTimelineId::new(t.parse().ok()?, r.parse().ok()?)
} Try / catch
let ttid = s.parse::<TenantTimelineId>().map_err(|e| {
anyhow::anyhow!("invalid TenantTimelineId '{s}', expected '<tenant_id>/<timeline_id>': {e}")
})?; Prevention
- 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
When it happens
Trigger: 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 '/'.
Common situations: Hand-written scripts exporting truncated tenant/timeline env vars; templated URLs where the tenant placeholder expanded to nothing.
Related errors
- TenantTimelineId must contain timeline_id
- TenantTimelineId must contain only tenant_id and timeline_id
- could not parse config file: {}
- could not open config file at path: {}
- could not parse spec
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/7cb35801532eb428.
Report an issue: GitHub.