block/buzz · error · anyhow::Error
target event not found
Error message
target event not found
What it means
In the relay's kind:9005 (NIP-29 delete event) side-effect handler, the target message is taken from the event's e tag and looked up with db.get_event_by_id scoped to the community. The path fails closed: if no stored event with that id exists, the delete is rejected before anything is stored — you cannot delete (or tombstone) a message the relay never accepted. This also guards against cross-community ids: an id that exists on another relay is still 'not found' here.
Solutions
- Verify the target exists on this relay before sending: query it (buzz messages thread --channel <ch> --event <id>, or POST /query with an ids filter).
- If the row was purged by deletion tooling, no delete is needed — the message is already gone; drop the request.
- Check the e tag is the full 64-char hex id with no whitespace or truncation.
- Confirm client and tooling talk to the same community (same RELAY_URL host) — an id from another community will never resolve.
Example fix
// before: fire-and-forget delete against an id that may be gone
await relay.publish(deleteEvent({ target: maybeStaleId, channel }));
// after: verify existence first, treat 9005 'target event not found' as success for purged ids
const existing = await relay.query({ ids: [targetId], kinds: [MESSAGE_KINDS] });
if (existing.length === 0) { /* already gone; nothing to delete */ } Defensive patterns
Strategy: validation
Validate before calling
// client-side: confirm the target exists in this community before publishing 9005
const rows = await relay.query({ ids: [targetId], limit: 1 });
if (rows.length === 0) {
// treat as already-deleted: publish nothing, report success to the user
return { ok: true, noop: true };
} Try / catch
try {
await relay.publish(deleteEvent);
} catch (e) {
if (String(e).includes('target event not found')) {
// idempotent delete: the target is gone from this community — swallow
return { ok: true, noop: true };
}
throw e;
} Prevention
- Make delete flows idempotent: 'not found' after a purge is success, not an error.
- Build delete events from ids obtained by querying the same relay, never from another environment's data.
- After running buzz-deletion retention jobs, drop queued deletes for purged ids instead of replaying them.
When it happens
Trigger: A client publishes kind:9005 whose e tag references an id that was never stored on this community, was already hard-deleted/purged (e.g., by buzz-deletion), was truncated or mangled during copy, or belongs to a different relay/community.
Common situations: Retrying an old delete after the target was purged by retention tooling; a client replaying a queue of deletes against a fresh/restored database; pointing a staging client at production (or vice versa) so ids never match.
Related errors
- must be event author or channel owner/admin
- target event belongs to a different channel
- target event has no channel
- actor is not an active member
- cannot remove the last owner
AI-assisted analysis of block/buzz@dad5a33865 (2026-09-05).
Data as JSON: /api/errors/68b8bc18dd807cd3.
Report an issue: GitHub.
Appendix: source
Thrown at crates/buzz-relay/src/handlers/side_effects.rs:271
let target_pubkey_bytes =
hex::decode(parts[1]).map_err(|_| anyhow::anyhow!("invalid pubkey in a-tag"))?;
if target_pubkey_bytes != actor_bytes
&& !state
.db
.is_agent_owner(tenant.community(), &target_pubkey_bytes, &actor_bytes)
.await?
{
return Err(anyhow::anyhow!("must be event author"));
}
return Ok(());
}
for target_id in target_ids {
let target_event = state
.db
.get_event_by_id_including_deleted_for_event_write(tenant.community(), &target_id)
.await?
.ok_or_else(|| anyhow::anyhow!("target event not found"))?;
let target_author =
effective_message_author(&target_event.event, &state.relay_keypair.public_key());
if target_author != actor_bytes
&& !state
.db
.is_agent_owner(tenant.community(), &target_author, &actor_bytes)
.await?
{
return Err(anyhow::anyhow!("must be event author"));
}
}
Ok(())
}
/// Returns `true` if `actor_bytes` is the NIP-OA owner of **any** active owner-role
/// member in `members`. Used by kind:9002 and kind:9008 to authorize the owningView on GitHub (pinned to dad5a33865)