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

  1. Pass manifest.etag (from get_action_manifest) directly as expected_etag, with no modification
  2. If you persist an etag between runs, store it unquoted and lowercase, exactly as delivered
  3. 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

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


AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17). Data as JSON: /api/errors/d287fd9cf9711a9c. Report an issue: GitHub.