{"record":{"id":"8f8a9e7906590172","repo":"astrid-runtime/astrid","slug":"revocation-kv-value-for-key-has-bytes-expe","errorCode":null,"errorMessage":"revocation KV value for {key:?} has {} bytes; expected 8","messagePattern":"revocation KV value for (.+?) has (.+?) bytes; expected 8","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/astrid-gateway/src/revocations.rs","lineNumber":104,"sourceCode":"            .open(path)\n            .with_context(|| format!(\"open legacy revocation file {}\", path.display()))?\n    };\n    #[cfg(not(unix))]\n    let file = std::fs::File::open(path)\n        .with_context(|| format!(\"open legacy revocation file {}\", path.display()))?;\n    let mut bytes = Vec::new();\n    file.take(MAX_REVOCATIONS_FILE_BYTES.saturating_add(1))\n        .read_to_end(&mut bytes)\n        .with_context(|| format!(\"read legacy revocation file {}\", path.display()))?;\n    if bytes.len() as u64 > MAX_REVOCATIONS_FILE_BYTES {\n        anyhow::bail!(\"legacy revocation file exceeds migration cap\");\n    }\n    Ok(bytes)\n}\n\nfn decode_epoch(bytes: &[u8], key: &str) -> anyhow::Result<u64> {\n    let raw: [u8; 8] = bytes.try_into().map_err(|_| {\n        anyhow::anyhow!(\n            \"revocation KV value for {key:?} has {} bytes; expected 8\",\n            bytes.len()\n        )\n    })?;\n    Ok(u64::from_le_bytes(raw))\n}\n\nfn encode_epoch(epoch: u64) -> Vec<u8> {\n    epoch.to_le_bytes().to_vec()\n}\n\n/// Record the maximum principal revocation epoch durably. The returned value\n/// is the epoch now authoritative in storage (which may be newer than the\n/// requested event when another writer won the CAS race).\npub async fn record_principal_max(\n    store: &dyn KvStore,\n    principal: &PrincipalId,\n    epoch: u64,","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-gateway/src/revocations.rs#L86-L122","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the offending KV entry and rewrite it as an 8-byte little-endian u64 epoch","Identify which writer produced the non-8-byte value (older version / external tool) and fix its encoding","Delete the corrupt entry if the revocation can be re-derived, then re-record it","Check for recent version changes in how revocation epochs are serialized"],"exampleFix":"// before\nstore.put(key, serde_json::to_vec(&epoch)?)?; // writes JSON text, not 8 bytes\n// after\nstore.put(key, epoch.to_le_bytes())?; // exactly 8 bytes, LE u64","handlingStrategy":"validation","validationCode":"fn is_epoch_value(bytes: &[u8]) -> bool { bytes.len() == 8 }\n// skip/log entries that fail is_epoch_value before calling record_*_max","typeGuard":"fn as_epoch(bytes: &[u8]) -> Option<u64> {\n    if bytes.len() == 8 { Some(u64::from_le_bytes(bytes.try_into().ok()?)) } else { None }\n}","tryCatchPattern":"match revocations::load_from_store(&store).await {\n    Ok(state) => state,\n    Err(e) if e.to_string().contains(\"expected 8\") => {\n        log::error!(\"corrupt revocation KV entry: {e}; rebuild or delete the entry\");\n        Err(e)\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Always write revocation epochs with u64::to_le_bytes","Add a store-level unit test asserting value length == 8 after writes","Reject/log foreign writers that put text/JSON into the revocation namespace"],"tags":["kv-store","encoding","data-corruption"],"backgroundTag":"invalid-argument-value","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}