{"record":{"id":"fa7cd3530704c2a2","repo":"jdx/mise","slug":"remote-cache-blob-pack-ended-before-a-blob-was-com","errorCode":null,"errorMessage":"remote cache blob pack ended before a blob was complete","messagePattern":"remote cache blob pack ended before a blob was complete","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/mise-cache-core/src/lib.rs","lineNumber":951,"sourceCode":"            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();\n            let count = reader.read(&mut buffer[..limit]).await?;\n            if count == 0 {\n                bail!(\"remote cache blob pack ended before a blob was complete\");\n            }\n            output.write_all(&buffer[..count]).await?;\n            hasher.update(&buffer[..count]);\n            remaining -= count as u64;\n        }\n        output.flush().await?;\n        drop(output);\n        if !hasher.matches(&digest.hash) {\n            bail!(\"remote cache blob pack failed digest verification\");\n        }\n        blobs.push((digest, path));\n    }\n    let blob_count = blobs.len().try_into().unwrap_or(u64::MAX);\n    let metadata = metadata.validate(BlobPackResponseStats {\n        blob_count,\n        payload_bytes,\n        framed_bytes,\n    })?;","sourceCodeStart":933,"sourceCodeEnd":969,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/crates/mise-cache-core/src/lib.rs#L933-L969","documentation":"While streaming a blob's payload, mise-cache-core reads exactly `digest.size` bytes declared in the entry header. If the underlying stream returns 0 bytes (a clean EOF, since I/O errors surface as Err) before the declared size is reached, the pack is truncated and this error aborts the download. It means the server's framing promised more bytes than its response body actually contained. Note the pack download already runs inside the crate's retry loop, but this bail! is not classified transient, so it surfaces on the first occurrence.","triggerScenarios":"A blob pack entry header declares size N but the response body ends after fewer bytes: the server wrote the header then failed to flush or skipped a short blob, a gateway truncated the body (Content-Length mismatch), or the connection closed mid-stream in a way that surfaced as EOF rather than an error.","commonSituations":"Server-side pack writers that write the size field before serializing the blob and then skip missing blobs; reverse proxies truncating large streaming responses; server buffer limits smaller than the pack being produced.","solutions":["Retry the fetch once: truncation is often a one-off connection cut.","Fix the server's pack writer to stream exactly `size` payload bytes after each header and flush before finishing the response; never skip a blob after emitting its header.","Verify no proxy between client and server truncates streaming responses or enforces a max body size smaller than your blobs.","Compare the response framing (Content-Length or chunk sizes) with the sum of entry headers to find which entry is short."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"let pack = download_with_retry(|| remote_cache.download(staging_dir)).await;\n// decode_blob_pack's bails are NOT retried by the crate's own retry loop,\n// so wrap the fetch: on \"ended before a blob was complete\", back off briefly\n// and re-request once; on a second failure, rebuild the artifacts locally.","preventionTips":["Keep blob pack responses below proxy/gateway body-size limits, or raise those limits for the pack endpoint.","Ensure the cache server flushes and completes the pack body exactly as framed (headers plus payloads).","Monitor for this error after server upgrades that touch the pack writer."],"tags":["remote-cache","blob-pack","truncated-response","streaming","rust"],"backgroundTag":"response-truncated","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}