astrid-runtime/astrid · error · anyhow::Error

revocation KV value for

Error message

revocation KV value for {key:?} has {} bytes; expected 8

What it means

decode_epoch converts a revocation KV value into a u64 (little-endian epoch seconds) and requires exactly 8 bytes. If the stored value for the key has any other length, the try_into::<[u8;8]>() fails and this error reports the actual byte count. It indicates a corrupt or foreign entry in the revocation KV namespace.

Solutions

  1. Inspect the offending KV entry and rewrite it as an 8-byte little-endian u64 epoch
  2. Identify which writer produced the non-8-byte value (older version / external tool) and fix its encoding
  3. Delete the corrupt entry if the revocation can be re-derived, then re-record it
  4. Check for recent version changes in how revocation epochs are serialized

Example fix

// before
store.put(key, serde_json::to_vec(&epoch)?)?; // writes JSON text, not 8 bytes
// after
store.put(key, epoch.to_le_bytes())?; // exactly 8 bytes, LE u64
Defensive patterns

Strategy: validation

Validate before calling

fn is_epoch_value(bytes: &[u8]) -> bool { bytes.len() == 8 }
// skip/log entries that fail is_epoch_value before calling record_*_max

Type guard

fn as_epoch(bytes: &[u8]) -> Option<u64> {
    if bytes.len() == 8 { Some(u64::from_le_bytes(bytes.try_into().ok()?)) } else { None }
}

Try / catch

match revocations::load_from_store(&store).await {
    Ok(state) => state,
    Err(e) if e.to_string().contains("expected 8") => {
        log::error!("corrupt revocation KV entry: {e}; rebuild or delete the entry");
        Err(e)
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling decode_epoch (via record_principal_max, record_device_max, or load_from_store) on a KV value whose length != 8 — e.g. a value written by a different format/version, a JSON or text value instead of the raw 8-byte LE u64, or truncated/corrupted storage.

Common situations: Manual edits or imports of KV entries with wrong encoding; an older gateway version writing a different serialization; corrupted store after a failed migration; someone storing JSON like '"1700000000"' instead of raw bytes.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/8f8a9e7906590172. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-gateway/src/revocations.rs:104

            .open(path)
            .with_context(|| format!("open legacy revocation file {}", path.display()))?
    };
    #[cfg(not(unix))]
    let file = std::fs::File::open(path)
        .with_context(|| format!("open legacy revocation file {}", path.display()))?;
    let mut bytes = Vec::new();
    file.take(MAX_REVOCATIONS_FILE_BYTES.saturating_add(1))
        .read_to_end(&mut bytes)
        .with_context(|| format!("read legacy revocation file {}", path.display()))?;
    if bytes.len() as u64 > MAX_REVOCATIONS_FILE_BYTES {
        anyhow::bail!("legacy revocation file exceeds migration cap");
    }
    Ok(bytes)
}

fn decode_epoch(bytes: &[u8], key: &str) -> anyhow::Result<u64> {
    let raw: [u8; 8] = bytes.try_into().map_err(|_| {
        anyhow::anyhow!(
            "revocation KV value for {key:?} has {} bytes; expected 8",
            bytes.len()
        )
    })?;
    Ok(u64::from_le_bytes(raw))
}

fn encode_epoch(epoch: u64) -> Vec<u8> {
    epoch.to_le_bytes().to_vec()
}

/// Record the maximum principal revocation epoch durably. The returned value
/// is the epoch now authoritative in storage (which may be newer than the
/// requested event when another writer won the CAS race).
pub async fn record_principal_max(
    store: &dyn KvStore,
    principal: &PrincipalId,
    epoch: u64,

View on GitHub (pinned to affd8760f4)