neondatabase/neon · error

Path {relative_path:?} is not relative

Error message

Path {relative_path:?} is not relative

What it means

RemotePath is the library's typed relative path into a remote bucket or local storage root; RemotePath::new enforces relativity via Utf8Path::is_relative() and rejects anything absolute — a leading '/', a Windows drive prefix, or any rooted path. This is input validation: the path is later joined onto a base directory/bucket prefix, so an absolute component would escape or corrupt the join.

Source

Thrown at libs/remote_storage/src/lib.rs:127

    where
        D: serde::Deserializer<'de>,
    {
        let str = String::deserialize(deserializer)?;
        Ok(Self(Utf8PathBuf::from(&str)))
    }
}

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

impl RemotePath {
    pub fn new(relative_path: &Utf8Path) -> anyhow::Result<Self> {
        anyhow::ensure!(
            relative_path.is_relative(),
            "Path {relative_path:?} is not relative"
        );
        Ok(Self(relative_path.to_path_buf()))
    }

    pub fn from_string(relative_path: &str) -> anyhow::Result<Self> {
        Self::new(Utf8Path::new(relative_path))
    }

    pub fn with_base(&self, base_path: &Utf8Path) -> Utf8PathBuf {
        base_path.join(&self.0)
    }

    pub fn object_name(&self) -> Option<&str> {
        self.0.file_name()
    }

    pub fn join(&self, path: impl AsRef<Utf8Path>) -> Self {
        Self(self.0.join(path))

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Strip leading separators before constructing: path.trim_start_matches('/')
  2. Build paths by joining components (Utf8PathBuf::push / join) rather than string concatenation
  3. Validate and normalize paths at the API boundary before they reach storage code
  4. On Windows also reject drive prefixes and UNC paths; treat a leading '/' as the canonical bad case

Example fix

// before: leading slash from user input makes is_relative() false
let path = RemotePath::from_string(&format!("/{}/{}", tenant_id, file_name))?;

// after: normalize before constructing
let relative = format!("{}/{}", tenant_id, file_name);
let path = RemotePath::from_string(relative.trim_start_matches('/'))?;
Defensive patterns

Strategy: validation

Validate before calling

// Normalize and validate before constructing a RemotePath.
fn to_remote_path(raw: &str) -> anyhow::Result<RemotePath> {
    let normalized = raw.trim_start_matches('/');
    anyhow::ensure!(!normalized.is_empty(), "path {raw:?} is empty after normalization");
    RemotePath::from_string(normalized)
}

Type guard

fn is_valid_remote_path(raw: &str) -> bool {
    let p = Utf8Path::new(raw.trim_start_matches('/'));
    !raw.starts_with(|c: char| c.is_ascii_alphabetic() && raw[1..].starts_with(':')) // windows drive
        && p.is_relative()
        && !p.has_root()
        && p.components().next().is_some()
        && !p.components().any(|c| matches!(c, std::path::Component::ParentDir))
}

Try / catch

// Construct paths via a single validated entry point; catch and reject with a clear message.
pub fn parse_user_path(raw: &str) -> Result<RemotePath, String> {
    RemotePath::from_string(raw.trim_start_matches('/'))
        .map_err(|e| format!("invalid storage path {raw:?}: {e}; paths must be relative, no leading '/'"))
}

Prevention

When it happens

Trigger: Constructing RemotePath from user input or URL fragments that start with '/'; passing a fully-qualified local path; formatting paths with a leading separator (format!(\"/{}/file\", tenant)); on Windows, paths with a drive letter or UNC prefix.

Common situations: Parsing request paths off an HTTP API and forwarding them verbatim; joining components via string concatenation with slashes; porting code that historically stored absolute paths; double-prefixed paths from config (base + '/absolute').

Related errors


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