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
- Strip trailing slashes before parsing: s.trim_end_matches('/')
- Check for accidental inclusion of a path prefix or suffix in the string
- 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
- Trim trailing slashes before parsing
- Never pass URL paths where a bare id pair is expected
- Use split_once-based construction instead of string concatenation
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
- TenantTimelineId must contain tenant_id
- TenantTimelineId must contain 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/3b80e55e3d7f986d.
Report an issue: GitHub.