block/buzz · error · IngestError::Rejected
invalid action_id tag
Error message
invalid action_id tag
What it means
Thrown by kind:9005 (DELETE_EVENT) when an optional action_id tag is present but does not parse as a UUID. The relay validates the tag whenever it exists — an absent action_id is fine, but a malformed one is rejected before the delete proceeds.
Source
Thrown at crates/buzz-relay/src/handlers/side_effects.rs:634
}
}
} else {
// topic/purpose: any member
let is_member = state
.is_member_cached(tenant.community(), channel_id, &actor_bytes)
.await?;
if is_member {
Ok(())
} else {
Err(anyhow::anyhow!("not a member"))
}
}
}
9005 => {
// DELETE_EVENT: event author OR channel owner/admin.
if let Some(action_id) = extract_tag_value(event, "action_id") {
Uuid::parse_str(&action_id)
.map_err(|_| anyhow::anyhow!("invalid action_id tag"))?;
}
// Extract target event from e tag to check authorship.
let target_id = event
.tags
.iter()
.find_map(|tag| {
if tag.kind().to_string() == "e" {
tag.content().and_then(|v| hex::decode(v).ok())
} else {
None
}
})
.ok_or_else(|| anyhow::anyhow!("missing e tag for target event"))?;
// Verify the target event exists and belongs to the h-tag channel
// BEFORE storage. Fail closed: missing target → reject.
let target_event = stateView on GitHub (pinned to f956e6fe06)
Solutions
- Omit the action_id tag if you do not need idempotency tracking — it is optional.
- Otherwise generate a real UUID (v4) and send it as a string: ["action_id", crypto.randomUUID()].
- Check for whitespace/case issues if hand-assembling; the parser is strict.
Example fix
// before
{ kind: 9005, tags: [["h", ch], ["e", targetId], ["action_id", `del-${Date.now()}`]] }
// after
{ kind: 9005, tags: [["h", ch], ["e", targetId], ["action_id", crypto.randomUUID()]] } Defensive patterns
Strategy: validation
Validate before calling
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
function actionIdTag(id?: string): string[] | null {
if (id === undefined) return null; // omitting it is fine
if (!UUID_RE.test(id)) throw new Error("action_id must be a UUID — got " + id);
return ["action_id", id];
} Type guard
function isUuid(v: string): boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);
} Try / catch
try {
await sdk.publish(deleteEvent);
} catch (e) {
if (String(e).includes("invalid action_id tag")) {
delete deleteEvent.tags.find((t) => t[0] === "action_id"); // drop the tag and retry
await sdk.publish(deleteEvent);
} else throw e;
} Prevention
- Generate action ids with crypto.randomUUID() — never timestamps or nanoids.
- Remember action_id is optional; omit it unless you need idempotency tracking.
- Validate ids from external sources (deep links, logs) before reusing them in events.
When it happens
Trigger: Publishing kind:9002/9005-style events with [["action_id", "delete"]], [["action_id", "12345"]], or a non-v4 string. Valid forms are UUIDs like "9b4c0d04-7a3e-4e2f-9d1a-2f6c8b5e7a11".
Common situations: Clients generating action ids with custom schemes (timestamps, nanoids, ULIDs) instead of UUIDs; copy-paste truncating the UUID; older SDK versions emitting a different id format.
Related errors
- missing e tag for target event
- target event belongs to a different channel
- target event has no channel
- kind:9002 must include at least one metadata tag (name, abou
- invalid archived value: {v} (must be "true" or "false")
AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16).
Data as JSON: /api/errors/34806705671bd7ad.
Report an issue: GitHub.