jdx/mise · error
remote action manifest ETag does not match its body
Error message
remote action manifest ETag does not match its body
What it means
Thrown on action-manifest GET (crates/mise-cache-core/src/lib.rs:723): the response must carry a strong ETag, and blake3 of the response body (hex) must equal that ETag string (parse_strong_etag extracts it). If hashing the body does not reproduce the ETag, the manifest body and its validator disagree — the client refuses to trust the bytes.
Solutions
- If you operate the server, set ETag to the strong blake3 hex of the exact bytes written: format!("\"{}\"", blake3::hash(&bytes).to_hex())
- Ensure no intermediary recompresses or rewrites manifest responses for the action-manifests routes
- If using a generic HTTP framework's auto-ETag, disable it and compute the blake3 ETag explicitly
- Retry once to rule out an in-flight body mutation race, then report to the server operator
Defensive patterns
Strategy: try-catch
Validate before calling
// server-side helper to guarantee agreement:
fn manifest_response(bytes: &[u8]) -> Response {
let etag = format!("\"{}\"", blake3::hash(bytes).to_hex());
Response::new(bytes).with_header(ETAG, etag)
} Try / catch
match client.get_action_manifest(&key).await {
Err(e) if e.to_string().contains("ETag does not match") => {
// body untrusted: treat as miss and re-upload the manifest
client.put_action_manifest(&key, &local_bytes, None).await?;
}
other => other?,
} Prevention
- Compute ETags from the exact serialized bytes at write time, never from a pre-serialization value
- Disable framework auto-ETag (mtime/len based) on manifest routes
- Keep manifest responses uncompressed or ensure ETag covers the same byte stream the client receives
When it happens
Trigger: Calling get_action_manifest where the server's ETag header is not the blake3 hex of the exact body — e.g. the server computes the ETag before final serialization, re-serializes differently (key order, whitespace), uses a weak ETag or an md5-based one, or a proxy modifies the body (recompression) after the ETag was generated.
Common situations: Custom servers deriving ETags from mtime or len rather than content; serde/JSON re-serialization changing byte layout between ETag computation and write; compression in transit; server frameworks that append a suffix to ETags.
Related errors
- remote action manifest keys must use blake3
- action prediction payload is too large
- cache agent returned an incomplete blob lookup response
- cached failed digest verification
- cached OCI layer diff ID mismatch
AI-assisted analysis of jdx/mise@6f52dcdf99 (2026-08-22).
Data as JSON: /api/errors/3434eb3c06a8a42e.
Report an issue: GitHub.
Appendix: source
Thrown at crates/mise-cache-core/src/lib.rs:723
let url = self.action_manifest_endpoint(key)?;
retry_async("GET", &url, self.retries, || async {
let response = self
.request(
reqwest::Method::GET,
url.clone(),
TASK_ACTION_MANIFEST_MEDIA_TYPE,
)
.await?
.send()
.await?;
if response.status() == StatusCode::NOT_FOUND {
return Ok(None);
}
let response = response.error_for_status()?;
let etag = parse_strong_etag(response.headers().get(ETAG))?;
let bytes = response.bytes().await?.to_vec();
if blake3::hash(&bytes).to_hex().as_str() != etag {
bail!("remote action manifest ETag does not match its body");
}
Ok(Some(RemoteActionManifest { bytes, etag }))
})
.await
}
pub async fn put_action_manifest(
&self,
key: &CacheDigest,
bytes: &[u8],
expected_etag: Option<&str>,
) -> Result<ManifestPutOutcome> {
let url = self.action_manifest_endpoint(key)?;
let body = bytes.to_vec();
let expected_etag = expected_etag.map(quoted_etag).transpose()?;
retry_async("PUT", &url, self.retries, || async {
let mut request = self
.request(View on GitHub (pinned to 6f52dcdf99)