{"record":{"id":"43c5243a0e298c8a","repo":"jdx/mise","slug":"remote-cache-blob-pack-returned-an-unrequested-dig","errorCode":null,"errorMessage":"remote cache blob pack returned an unrequested digest","messagePattern":"remote cache blob pack returned an unrequested digest","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/mise-cache-core/src/lib.rs","lineNumber":930,"sourceCode":"        let (algorithm, mut hasher) = match algorithm[0] {\n            1 => (\n                \"blake3\",\n                BlobPackHasher::Blake3(Box::new(blake3::Hasher::new())),\n            ),\n            2 => (\"sha256\", BlobPackHasher::Sha256(sha2::Sha256::new())),\n            _ => bail!(\"remote cache blob pack has an invalid digest algorithm\"),\n        };\n        let mut hash = [0_u8; 32];\n        reader.read_exact(&mut hash).await?;\n        let mut size = [0_u8; 8];\n        reader.read_exact(&mut size).await?;\n        let digest = CacheDigest {\n            algorithm: algorithm.into(),\n            hash: hex::encode(hash),\n            size: u64::from_be_bytes(size),\n        };\n        if !requested.contains(&digest) {\n            bail!(\"remote cache blob pack returned an unrequested digest\");\n        }\n        if !seen.insert(digest.clone()) {\n            bail!(\"remote cache blob pack returned a duplicate digest\");\n        }\n        framed_bytes = framed_bytes\n            .checked_add(BLOB_PACK_HEADER_BYTES)\n            .and_then(|bytes| bytes.checked_add(digest.size))\n            .ok_or_else(|| eyre!(\"remote cache blob pack is too large\"))?;\n        payload_bytes = payload_bytes\n            .checked_add(digest.size)\n            .ok_or_else(|| eyre!(\"remote cache blob pack payload is too large\"))?;\n\n        let path = directory.path().join(blobs.len().to_string());\n        let mut output = tokio::fs::File::create(&path).await?;\n        let mut remaining = digest.size;\n        let mut buffer = [0_u8; 64 * 1024];\n        while remaining > 0 {\n            let limit = usize::try_from(remaining.min(buffer.len() as u64)).unwrap();","sourceCodeStart":912,"sourceCodeEnd":948,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/crates/mise-cache-core/src/lib.rs#L912-L948","documentation":"Thrown while decoding a blob pack streamed from the remote build cache. For every entry, mise-cache-core reads the framing header (one algorithm byte, a 32-byte hash, an 8-byte big-endian size), rebuilds a CacheDigest, and requires it to be one of the digests that were POSTed in the DigestList request body. If the server includes a blob that was never requested, decoding aborts immediately so unrequested content never reaches the local content-addressed store. This is a server-contract violation: the blob pack endpoint may only return blobs from the request list.","triggerScenarios":"POSTing a digest list to the blob pack endpoint (the remote cache's missing-blob download after an action cache hit) and the response stream contains an entry whose (algorithm, hash, size) triple is not in that list. Typical producers: a server that assembles the pack from its own inventory instead of the request, a proxy/CDN serving a cached POST response from a different request, or a pack-format framing mismatch between client and server versions.","commonSituations":"Running a custom or third-party cache server that ignores the request digest list; an HTTP proxy that reuses cached POST responses; client/server version skew where entry headers are framed differently so digests get misparsed mid-stream.","solutions":["Verify the cache server implements the pack contract: iterate only the requested digests and frame each entry as algorithm byte + 32-byte hash + 8-byte big-endian size after the magic header.","Disable POST-response caching on any proxy/CDN in front of the blob pack endpoint.","Capture the request DigestList body and the response stream, then diff entry digests against the request to identify the unrequested entry.","If the server is third-party, pin client and server to versions tested together, or make the pack endpoint return 404/405/501 so the client falls back to per-blob GETs.","As a last resort, clear the affected cache entries so the pack is rebuilt from a known-good state."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"let pack = match remote_cache.download(staging_dir).await {\n    Ok(pack) => pack,\n    Err(err) if err.to_string().contains(\"blob pack\") => {\n        // server violated the pack contract; treat as a cache miss\n        warn!(\"invalid blob pack: {err}\");\n        return recompute_locally();\n    }\n    Err(err) => return Err(err),\n};","preventionTips":["Validate the cache server against the pack round-trip tests in crates/mise-cache-core before enabling blob packs.","Do not put a caching reverse proxy in front of the blob pack POST endpoint.","Pin the cache server and mise-cache-core to versions tested together."],"tags":["remote-cache","blob-pack","protocol-violation","rust","content-addressable-storage"],"backgroundTag":"server-protocol-violation","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}