neondatabase/neon · error

TenantTimelineId must contain timeline_id

Error message

TenantTimelineId must contain timeline_id

What it means

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.

Source

Thrown at libs/utils/src/id.rs:342

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);

impl fmt::Display for NodeId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Append the missing '/<timeline_id>' to form the two-part id
  2. Source both ids from the console/CLI and join them with '/'
  3. Validate the format at the config boundary with a split-based check

Example fix

// before
let ttid: TenantTimelineId = tenant_id.to_string().parse()?;
// after
let ttid: TenantTimelineId = format!("{tenant_id}/{timeline_id}").parse()?;
Defensive patterns

Strategy: type-guard

Validate before calling

fn has_both_components(s: &str) -> bool {
    s.split('/').count() == 2
}

anyhow::ensure!(has_both_components(s), "expected '<tenant_id>/<timeline_id>', got '{s}'");

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 = match s.parse::<TenantTimelineId>() {
    Ok(id) => id,
    Err(e) => anyhow::bail!("'{s}' is not '<tenant_id>/<timeline_id>': {e}"),
};

Prevention

When it happens

Trigger: 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).

Common situations: Passing only NEON_TIMELINE_ID where a pair is expected; copy-paste that dropped the second half after the slash.

Related errors


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