neondatabase/neon · error

TenantTimelineId must contain only tenant_id and timeline_id

Error message

TenantTimelineId must contain only tenant_id and timeline_id

What it means

TenantTimelineId::from_str requires exactly two '/'-separated components; if a third component exists (parts.next() returns Some), parsing fails. Common shapes include 'a/b/c' and the easy-to-miss trailing slash 'a/b/', which yields an empty third component.

Source

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

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

impl FromStr for NodeId {

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Strip trailing slashes before parsing: s.trim_end_matches('/')
  2. Check for accidental inclusion of a path prefix or suffix in the string
  3. Join components explicitly with exactly one '/'

Example fix

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

Strategy: type-guard

Validate before calling

fn has_exactly_two_components(s: &str) -> bool {
    let trimmed = s.trim_end_matches('/');
    trimmed.split('/').count() == 2 && !trimmed.ends_with('/')
}

anyhow::ensure!(has_exactly_two_components(s), "expected exactly '<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.trim_end_matches('/').parse::<TenantTimelineId>().map_err(|e| {
    anyhow::anyhow!("'{s}' must be exactly '<tenant_id>/<timeline_id>': {e}")
})?;

Prevention

When it happens

Trigger: Parsing an id string with more than one '/', e.g. 'tenant/timeline/' (trailing slash from shell completion or string joins), 'tenant/timeline/extra', or URLs accidentally passed whole.

Common situations: Trailing slashes appended by path-joining helpers; passing a URL path fragment instead of the bare pair; string concatenation bugs adding extra separators.

Related errors


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