jdx/mise · error · eyre::Report
invalid remote action manifest ETag
Error message
invalid remote action manifest ETag
What it means
put_action_manifest's expected_etag parameter must be the unquoted, 64-character lowercase blake3 hex digest — exactly the string returned in RemoteActionManifest.etag by get_action_manifest. quoted_etag() validates the caller-supplied value before the request is sent and rejects anything else: surrounding quotes, a W/ weak-ETag prefix, uppercase hex, or a wrong-length string.
Source
Thrown at crates/mise-cache-core/src/lib.rs:572
.await
}
}
fn parse_strong_etag(value: Option<&HeaderValue>) -> Result<String> {
let value = value
.and_then(|value| value.to_str().ok())
.ok_or_else(|| eyre!("remote action manifest response is missing an ETag"))?;
let etag = value
.strip_prefix('"')
.and_then(|value| value.strip_suffix('"'))
.filter(|value| is_lower_hex_digest(value))
.ok_or_else(|| eyre!("remote action manifest response has an invalid ETag"))?;
Ok(etag.to_owned())
}
fn quoted_etag(etag: &str) -> Result<HeaderValue> {
if !is_lower_hex_digest(etag) {
bail!("invalid remote action manifest ETag");
}
Ok(HeaderValue::from_str(&format!("\"{etag}\""))?)
}
fn is_lower_hex_digest(value: &str) -> bool {
value.len() == 64
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
#[derive(Clone)]
enum RemoteCacheCredential {
None,
Static(HeaderValue),
File(PathBuf),
GithubActions(Arc<GithubActionsOidcCredential>),
}View on GitHub (pinned to 9dcfcaa0dc)
Solutions
- Pass manifest.etag (from get_action_manifest) directly as expected_etag, with no modification
- If you persist an etag between runs, store it unquoted and lowercase, exactly as delivered
- Never build expected_etag from raw HTTP response headers; source it exclusively from this crate's API
Example fix
// before: forwarding a quoted header value
client
.put_action_manifest(&key, &bytes, Some(&format!("\"{etag}\"")))
.await?;
// after: pass the etag exactly as get_action_manifest returned it
client
.put_action_manifest(&key, &bytes, Some(manifest.etag.as_str()))
.await?; Defensive patterns
Strategy: validation
Validate before calling
fn is_unquoted_blake3_etag(etag: &str) -> bool {
!etag.starts_with('"')
&& !etag.starts_with("W/")
&& etag.len() == 64
&& etag.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
// gate the call before put_action_manifest
if let Some(etag) = &expected_etag {
assert!(is_unquoted_blake3_etag(etag), "etag must come from get_action_manifest");
} Type guard
fn is_valid_expected_etag(etag: Option<&str>) -> bool {
etag.map_or(true, |e| {
e.len() == 64 && e.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
})
} Prevention
- Always pass RemoteActionManifest.etag verbatim as expected_etag
- Never construct expected_etag from raw response headers
- If persisting etags, store them unquoted/lowercase and re-validate on load
When it happens
Trigger: Passing the raw ETag response-header value (which includes quotes) verbatim; wrapping the etag in quotes yourself; using a weak ETag (W/"..."); passing a sha256-based etag; persisting the etag through a layer that uppercases hex.
Common situations: Round-tripping etags through a proxy or cache layer that keeps header quoting; storing etags in a database with case-folding; hand-building the optimistic-concurrency update instead of using the value from get_action_manifest.
Related errors
- remote action manifest ETag does not match its body
- unsupported remote cache digest algorithm
- invalid remote cache digest
- remote action manifest keys must use blake3
- remote cache URL must use HTTPS
AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17).
Data as JSON: /api/errors/d287fd9cf9711a9c.
Report an issue: GitHub.