{"record":{"id":"9776a3d8330bb4ac","repo":"astrid-runtime/astrid","slug":"persist-device-revocation-fence-for-key-id-err","errorCode":null,"errorMessage":"persist device revocation fence for {key_id}: {error}","messagePattern":"persist device revocation fence for (.+?): (.+?)","errorType":"http","errorClass":"GatewayError::Internal","httpStatus":500,"severity":"critical","filePath":"crates/astrid-gateway/src/routes/principals.rs","lineNumber":575,"sourceCode":"/// The kernel has already removed the public key when this helper runs. The\n/// gateway only emits HTTP 204 after the same monotonic device fence is\n/// durably published and installed in its live map. Keeping this narrow step\n/// separate also lets persistence-failure tests exercise the HTTP boundary\n/// without fabricating a kernel admin client.\npub async fn acknowledge_device_revocation(\n    state: &GatewayState,\n    key_id: &str,\n    epoch: u64,\n) -> GatewayResult<StatusCode> {\n    crate::revocations::apply_device_revocation(\n        &state.revoked_key_ids,\n        state.storage_kv.as_deref(),\n        key_id,\n        epoch,\n    )\n    .await\n    .map_err(|error| {\n        GatewayError::Internal(anyhow::anyhow!(\n            \"persist device revocation fence for {key_id}: {error}\"\n        ))\n    })?;\n    Ok(StatusCode::NO_CONTENT)\n}\n\n// ── Helpers ──────────────────────────────────────────────────────\n\npub(crate) fn caller_from(req: &Request<axum::body::Body>) -> GatewayResult<&CallerContext> {\n    req.extensions()\n        .get::<CallerContext>()\n        .ok_or(GatewayError::Unauthorized)\n}\n\n// Both helpers consume their argument logically (wrap and discard);\n// clippy::needless_pass_by_value fires because they only `Display` /\n// `Debug` the value. Taking by value keeps `map_err(daemon_internal)`\n// usable as a one-line closure replacement throughout the routes —","sourceCodeStart":557,"sourceCodeEnd":593,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-gateway/src/routes/principals.rs#L557-L593","documentation":"`acknowledge_device_revocation` throws this GatewayError::Internal when `crate::revocations::apply_device_revocation` fails to durably persist (and install in the live map) the device revocation fence for the given key_id. The kernel has already removed the device's public key, but the gateway-side fence could not be written (e.g. to storage_kv), so the gateway refuses to return 204 because the revocation acknowledgement is incomplete — the device could otherwise still be treated as paired by the gateway.","triggerScenarios":"DELETE /api/principals/{id}/devices/{key_id} where the kernel admin PairDeviceRevoke succeeds but `apply_device_revocation` errors — typically because `state.storage_kv` is unavailable/misconfigured or the underlying KV write fails (I/O error, permission denied, serialization failure).","commonSituations":"storage_kv path misconfigured or on a read-only filesystem; KV backend down or disk full; corrupted KV record for the key_id; gateway running with storage_kv = None in an environment that requires durable revocation persistence; transient storage outage during a revoke request.","solutions":["Inspect the inner error chained to this message — it names the underlying apply_device_revocation failure (KV path, permissions, backend connectivity) and fix that first.","Verify `state.storage_kv` points to a writable, correctly configured KV store; restart the gateway once storage is healthy.","Re-run the device deletion after fixing storage; ensure the fence can be re-applied idempotently rather than leaving the gateway's revoked-key map stale.","Monitor/alert on KV write failures so revocation fences are never left unacknowledged in production."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// before revoking, confirm durable storage is available\nif state.storage_kv.is_none() {\n    return Err(anyhow!(\"storage_kv not configured; cannot persist revocation fence\"));\n}","typeGuard":"fn storage_ready(state: &GatewayState) -> bool {\n    state.storage_kv.is_some()\n}","tryCatchPattern":"match acknowledge_device_revocation(&state, &key_id, epoch).await {\n    Err(e) if e.to_string().starts_with(\"persist device revocation fence\") => {\n        // kernel already revoked; enqueue fence for retry and surface 503\n        tracing::warn!(key_id, error = %e, \"revocation fence not persisted\");\n        StatusCode::SERVICE_UNAVAILABLE\n    }\n    other => other?,\n}","preventionTips":["Health-check the KV backend before serving delete-device requests","Make apply_device_revocation idempotent so a retry after partial failure can complete the fence","Alert on revocation persistence failures — a missed fence lets a revoked device remain usable at the gateway"],"tags":["rust","gateway","persistence","security"],"backgroundTag":"database-write-failed","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}