jdx/mise · error

remote cache directory is not canonical JSON

Error message

remote cache directory is not canonical JSON

What it means

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.

Source

Thrown at src/task/task_cache_store.rs:720

        mode: u32,
        target: PathBuf,
    },
}

async fn materialize_remote_tree(
    store: &HttpTaskCacheStore,
    root: &CacheDigest,
) -> Result<tempfile::NamedTempFile> {
    let mut pending = vec![(PathBuf::new(), root.clone(), BTreeSet::new())];
    let mut nodes = BTreeMap::<PathBuf, RestoredNode>::new();
    while let Some((path, digest, mut ancestors)) = pending.pop() {
        if !ancestors.insert(digest.clone()) {
            bail!("remote cache directory graph contains a cycle");
        }
        let bytes = store.client.get_blob(&digest, DIRECTORY_MEDIA_TYPE).await?;
        let directory: RemoteDirectory = serde_json::from_slice(&bytes)?;
        if canonical_json(&serde_json::to_value(&directory)?)? != bytes {
            bail!("remote cache directory is not canonical JSON");
        }
        if directory.version != 1 {
            bail!("unsupported remote cache directory version");
        }
        let mut names = BTreeSet::new();
        for directory in directory.directories {
            validate_cache_name(&directory.name)?;
            if !names.insert(directory.name.clone()) {
                bail!("remote cache directory contains duplicate names");
            }
            let child = path.join(&directory.name);
            validate_cache_path(&child)?;
            nodes.insert(
                child.clone(),
                RestoredNode::Directory {
                    mode: directory.mode,
                },
            );

View on GitHub (pinned to 9dcfcaa0dc)

Solutions

  1. Delete the non-canonical blobs and let a current mise re-upload them
  2. Ensure nothing between mise and the storage rewrites request/response bodies
  3. Pin one mise version for all cache writers and upgrade them together
  4. If you operate the CAS, byte-validate blobs at upload time against the client's canonical form
Defensive patterns

Strategy: fallback

Try / catch

match restore_from_remote(&key).await {
    Ok(hit) => hit,
    Err(err) if err.to_string().contains("not canonical JSON") => {
        warn!("non-canonical blob at digest; purging and re-uploading");
        purge_blobs_for(&key).await.ok();
        run_task_uncached_then_upload(&task).await?
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: 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.

Common situations: A proxy between mise and storage that normalizes JSON bodies; custom scripts uploading directory protos; version skew across mise releases changing serde output.

Related errors


AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17). Data as JSON: /api/errors/a0d8529dab591cca. Report an issue: GitHub.