nautechsystems/nautilus_trader · error · anyhow::Error

Cross-store URI {path} (root {}) does not belong to catalog

Error message

Cross-store URI {path} (root {}) does not belong to catalog rooted at {} ({})

What it means

After confirming the URI scheme is remote, `remote_uri_object_path` compares the store root of the given path (bucket/container) with the store root of the catalog's `original_uri`. If they differ, the path belongs to another object store and would cross store boundaries, so the operation is rejected. The catalog only operates on data under its own root store.

Source

Thrown at crates/persistence/src/backend/catalog.rs:3064

        Ok(self.path_without_local_base(&normalized_path))
    }

    fn remote_uri_object_path(&self, path: &str) -> anyhow::Result<String> {
        let path_url = url::Url::parse(path)
            .map_err(|e| anyhow::anyhow!("Failed to parse object store URI {path}: {e}"))?;
        if !is_remote_uri_scheme(path_url.scheme()) {
            anyhow::bail!(
                "URI {path} uses non-remote scheme {} for remote catalog at {}",
                path_url.scheme(),
                self.original_uri,
            );
        }

        let catalog_root = remote_store_root_url(&self.original_uri)?;
        let path_root = remote_store_root_url(path)?;
        if catalog_root.as_str().trim_end_matches('/') != path_root.as_str().trim_end_matches('/') {
            anyhow::bail!(
                "Cross-store URI {path} (root {}) does not belong to catalog rooted at {} ({})",
                path_root.as_str().trim_end_matches('/'),
                self.original_uri,
                catalog_root.as_str().trim_end_matches('/'),
            );
        }

        // The URL crate keeps the path component percent-encoded (e.g. `%5E`),
        // so preserve that encoding for `ObjectPath::parse` round-trips through
        // `object_store::list`/`get`.
        Ok(path_url.path().trim_start_matches('/').to_string())
    }

    fn path_without_local_base(&self, path: &str) -> String {
        let base_path = if self.base_path.is_empty() {
            self.native_base_path_string()
        } else {
            self.base_path.clone()

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Point the path at the same bucket/container root as the catalog URI (compare the roots in the error message: 'root X' vs 'catalog rooted at Y').
  2. Re-create the catalog with the URI of the store you actually want to operate on, so the roots match.
  3. Move/copy the data into the catalog's root store if it legitimately belongs under this catalog.

Example fix

// before
catalog.object_store_path("gs://other-bucket/data/quotes.parquet"); // catalog rooted at s3://my-bucket
// after
catalog.object_store_path("s3://my-bucket/data/quotes.parquet");
Defensive patterns

Strategy: validation

Validate before calling

// Rust: check the path shares the catalog's store root before calling
let catalog_root = "s3://my-bucket";
let path_root = path.split("/").take(3).collect::<Vec<_>>().join("/");
assert_eq!(catalog_root.trim_end_matches('/'), path_root.trim_end_matches('/'), "path must be under catalog root");

Prevention

When it happens

Trigger: Calling `catalog.object_store_path(path)` (via `remote_uri_object_path`) where the path's remote root (e.g. `s3://bucket-b`) differs from the catalog root (e.g. `s3://bucket-a`), including trailing-slash variants that would otherwise normalize identically.

Common situations: Copying paths between environments (staging bucket vs production bucket); multi-bucket setups where the catalog was initialized with one bucket but code references another; region-specific bucket names (`s3://us-east-data` vs `s3://eu-data`); switching cloud providers (GCS path against S3-rooted catalog).

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/635f3574b20144fb. Report an issue: GitHub.