{"record":{"id":"a0d8529dab591cca","repo":"jdx/mise","slug":"remote-cache-directory-is-not-canonical-json","errorCode":null,"errorMessage":"remote cache directory is not canonical JSON","messagePattern":"remote cache directory is not canonical JSON","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/task/task_cache_store.rs","lineNumber":720,"sourceCode":"        mode: u32,\n        target: PathBuf,\n    },\n}\n\nasync fn materialize_remote_tree(\n    store: &HttpTaskCacheStore,\n    root: &CacheDigest,\n) -> Result<tempfile::NamedTempFile> {\n    let mut pending = vec![(PathBuf::new(), root.clone(), BTreeSet::new())];\n    let mut nodes = BTreeMap::<PathBuf, RestoredNode>::new();\n    while let Some((path, digest, mut ancestors)) = pending.pop() {\n        if !ancestors.insert(digest.clone()) {\n            bail!(\"remote cache directory graph contains a cycle\");\n        }\n        let bytes = store.client.get_blob(&digest, DIRECTORY_MEDIA_TYPE).await?;\n        let directory: RemoteDirectory = serde_json::from_slice(&bytes)?;\n        if canonical_json(&serde_json::to_value(&directory)?)? != bytes {\n            bail!(\"remote cache directory is not canonical JSON\");\n        }\n        if directory.version != 1 {\n            bail!(\"unsupported remote cache directory version\");\n        }\n        let mut names = BTreeSet::new();\n        for directory in directory.directories {\n            validate_cache_name(&directory.name)?;\n            if !names.insert(directory.name.clone()) {\n                bail!(\"remote cache directory contains duplicate names\");\n            }\n            let child = path.join(&directory.name);\n            validate_cache_path(&child)?;\n            nodes.insert(\n                child.clone(),\n                RestoredNode::Directory {\n                    mode: directory.mode,\n                },\n            );","sourceCodeStart":702,"sourceCodeEnd":738,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/task/task_cache_store.rs#L702-L738","documentation":"After downloading a directory proto, mise re-serializes it through its canonical JSON serializer and byte-compares against what was fetched; any difference in key order, whitespace, or encoding bails with 'remote cache directory is not canonical JSON'. Because blobs are content-addressed, this pins the exact byte form mise wrote and catches re-encoded or forged blobs.","triggerScenarios":"The blob stored at a directory digest was pretty-printed, key-reordered, or re-encoded by an intermediary, or was written by a different mise version whose canonical serialization differs.","commonSituations":"A proxy between mise and storage that normalizes JSON bodies; custom scripts uploading directory protos; version skew across mise releases changing serde output.","solutions":["Delete the non-canonical blobs and let a current mise re-upload them","Ensure nothing between mise and the storage rewrites request/response bodies","Pin one mise version for all cache writers and upgrade them together","If you operate the CAS, byte-validate blobs at upload time against the client's canonical form"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"match restore_from_remote(&key).await {\n    Ok(hit) => hit,\n    Err(err) if err.to_string().contains(\"not canonical JSON\") => {\n        warn!(\"non-canonical blob at digest; purging and re-uploading\");\n        purge_blobs_for(&key).await.ok();\n        run_task_uncached_then_upload(&task).await?\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Keep intermediaries (proxies, transforms) from rewriting JSON bodies between mise and storage","Only mise itself should write blobs; no pretty-printers or re-serializers in the pipeline","Upgrade all cache-writing machines on the same mise version","Hash-verify blobs at the storage boundary so re-encodes are caught server-side"],"tags":["mise","remote-cache","task-cache","json","integrity"],"backgroundTag":"non-canonical-json","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}