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
- Append the missing '/<timeline_id>' to form the two-part id
- Source both ids from the console/CLI and join them with '/'
- 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
- Always join tenant and timeline ids with format!("{tenant}/{timeline}")
- Reject single-component values in input validation schemas
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
- TenantTimelineId must contain tenant_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/2819cac691ddade8.
Report an issue: GitHub.