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

  1. Use the canonical '<tenant_id>/<timeline_id>' form with both 32-char hex ids
  2. Copy the full pair from console/neon CLI output instead of typing it
  3. 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

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


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/7cb35801532eb428. Report an issue: GitHub.