{"record":{"id":"635f3574b20144fb","repo":"nautechsystems/nautilus_trader","slug":"cross-store-uri-path-root-does-not-belong-t","errorCode":null,"errorMessage":"Cross-store URI {path} (root {}) does not belong to catalog rooted at {} ({})","messagePattern":"Cross-store URI (.+?) \\(root (.+?)\\) does not belong to catalog rooted at (.+?) \\((.+?)\\)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/persistence/src/backend/catalog.rs","lineNumber":3064,"sourceCode":"\n        Ok(self.path_without_local_base(&normalized_path))\n    }\n\n    fn remote_uri_object_path(&self, path: &str) -> anyhow::Result<String> {\n        let path_url = url::Url::parse(path)\n            .map_err(|e| anyhow::anyhow!(\"Failed to parse object store URI {path}: {e}\"))?;\n        if !is_remote_uri_scheme(path_url.scheme()) {\n            anyhow::bail!(\n                \"URI {path} uses non-remote scheme {} for remote catalog at {}\",\n                path_url.scheme(),\n                self.original_uri,\n            );\n        }\n\n        let catalog_root = remote_store_root_url(&self.original_uri)?;\n        let path_root = remote_store_root_url(path)?;\n        if catalog_root.as_str().trim_end_matches('/') != path_root.as_str().trim_end_matches('/') {\n            anyhow::bail!(\n                \"Cross-store URI {path} (root {}) does not belong to catalog rooted at {} ({})\",\n                path_root.as_str().trim_end_matches('/'),\n                self.original_uri,\n                catalog_root.as_str().trim_end_matches('/'),\n            );\n        }\n\n        // The URL crate keeps the path component percent-encoded (e.g. `%5E`),\n        // so preserve that encoding for `ObjectPath::parse` round-trips through\n        // `object_store::list`/`get`.\n        Ok(path_url.path().trim_start_matches('/').to_string())\n    }\n\n    fn path_without_local_base(&self, path: &str) -> String {\n        let base_path = if self.base_path.is_empty() {\n            self.native_base_path_string()\n        } else {\n            self.base_path.clone()","sourceCodeStart":3046,"sourceCodeEnd":3082,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/persistence/src/backend/catalog.rs#L3046-L3082","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["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').","Re-create the catalog with the URI of the store you actually want to operate on, so the roots match.","Move/copy the data into the catalog's root store if it legitimately belongs under this catalog."],"exampleFix":"// before\ncatalog.object_store_path(\"gs://other-bucket/data/quotes.parquet\"); // catalog rooted at s3://my-bucket\n// after\ncatalog.object_store_path(\"s3://my-bucket/data/quotes.parquet\");","handlingStrategy":"validation","validationCode":"// Rust: check the path shares the catalog's store root before calling\nlet catalog_root = \"s3://my-bucket\";\nlet path_root = path.split(\"/\").take(3).collect::<Vec<_>>().join(\"/\");\nassert_eq!(catalog_root.trim_end_matches('/'), path_root.trim_end_matches('/'), \"path must be under catalog root\");","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Derive all data paths from the catalog's base URI instead of hardcoding buckets.","Use environment-specific config (one bucket per env) and read the bucket name from the same config as the catalog URI.","Log the catalog root at startup and keep path-building code in one shared helper."],"tags":["url","object-store","configuration","rust"],"backgroundTag":"invalid-url","analyzedSha":"18893faf8b356be3320add8de2f861b0b647cf06","analyzedAt":"2026-09-08T20:49:34.690Z","contentChangedAt":"2026-09-08T20:49:34.690Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}