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
- Strip leading separators before constructing: path.trim_start_matches('/')
- Build paths by joining components (Utf8PathBuf::push / join) rather than string concatenation
- Validate and normalize paths at the API boundary before they reach storage code
- 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
- Never build storage paths by string concatenation with '/' — join components with path APIs
- Reject or normalize absolute paths at the HTTP/API boundary before they reach storage code
- Add unit tests covering leading '/', '..' components, empty strings, and Windows drive prefixes
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
- `datadir` must be a directory when calling this function: {d
- too many shards: {n}
- invalid shard index {shard_id}, expected {count}
- unsupported linekind: {s}
- must specify exactly one target
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/362a6bf22b35279e.
Report an issue: GitHub.