jdx/mise · error

remote cache directory is not canonical JSON

Error message

remote cache directory is not canonical JSON

What it means

Remote directory blobs are required to be stored as canonical JSON (keys sorted, no insignificant whitespace). After deserializing and re-serializing the blob, mise compares the canonical form to the raw bytes; a mismatch means the blob was written by a non-conforming producer, so it bails rather than trusting it.

Solutions

  1. Re-upload the cache entries using mise itself (re-run the task) so blobs are written with canonical JSON.
  2. Fix the custom cache producer to serialize with mise's canonical JSON routine (sorted keys, compact output).
  3. Purge affected blobs from the remote store and let clients repopulate them.
  4. Check whether any storage layer is re-encoding JSON on write/read and disable that transformation.

Example fix

// before: pretty, non-canonical
serde_json::to_string_pretty(&directory)
// after: canonical form as required
serde_json::to_vec(&directory)
Defensive patterns

Strategy: validation

Validate before calling

const canonical = o => JSON.stringify(o, Object.keys(o).sort());
if (canonical(parsed) !== rawString) throw new Error('directory blob is not canonical JSON');

Prevention

When it happens

Trigger: get_blob returns a directory blob whose bytes differ from canonical_json(serde_json::to_value(directory)) — i.e. the blob was serialized with unsorted keys, extra whitespace, or other non-canonical formatting.

Common situations: A custom or older cache writer stored pretty-printed or non-deterministically ordered JSON; blobs were transformed/normalized by an intermediary (proxy, CDN) that re-serialized them; manual editing of remote blobs.

Related errors


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

Appendix: 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 533346cc37)