block/buzz · error · anyhow::Error
target event has no channel
Error message
target event has no channel
What it means
The kind:9005 delete handler found the e-tag target but its stored channel_id is NULL, meaning it is not a channel message — typically an addressable/control event such as kind:39000 metadata, a membership list, a DM wrapper, or other non-channel state. The message-delete path only applies to channel-scoped events, so the relay rejects it instead of guessing a channel from the h tag.
Solutions
- Use the target's kind to route: channel messages (e.g., kind 40002 and other message kinds) go through 9005; addressable channel state uses its own admin/replace flow (republish via reconcile-channels or the owning kind).
- Check what the id actually is before deleting: relay query with { ids: [id] } and inspect kind and tags.
- If the target is channel metadata that must change, publish a replacement event of the same kind/d tag instead of a 9005 delete.
- Fix client code that reuses one delete routine for all event types.
Example fix
// before: one delete path for everything
await publish(delete9005(targetId, currentChannel)); // fails when target is kind:39000
// after: route by target kind
const target = await relay.queryOne({ ids: [targetId] });
if (target.tags.some(([k]) => k === 'h')) {
await publish(delete9005(targetId, channelOf(target)));
} else {
await publish(replaceAddressable(target.kind, dTagOf(target), newContent)); // or admin op
} Defensive patterns
Strategy: type-guard
Type guard
// only channel-scoped events may enter the 9005 path: they carry an h tag
function isChannelMessage(ev: NostrEvent): boolean {
return ev.tags.some(([k]) => k === 'h');
} Prevention
- Route deletes by target kind: h-tagged messages -> 9005; addressable state -> republish the same kind/d tag.
- Inspect unknown ids with a { ids: [id] } query before choosing the operation.
- Keep a shared 'message kinds' constant list so generic delete helpers only accept message kinds.
When it happens
Trigger: Pointing a 9005 delete at a kind:39000/39001/39002 channel-definition event, a user-metadata event, or any event stored without channel assignment; using the generic delete flow where a channel-specific admin operation is required.
Common situations: Tooling that 'deletes anything by id' hitting control events; debugging with a message id that is actually the channel's d-tag addressable event.
Related errors
- target event belongs to a different channel
- must be event author or channel owner/admin
- target event not found
- actor is not an active member
- cannot remove the last owner
AI-assisted analysis of block/buzz@dad5a33865 (2026-08-20).
Data as JSON: /api/errors/33ff9f58bd442161.
Report an issue: GitHub.
Appendix: source
Thrown at crates/buzz-relay/src/handlers/side_effects.rs:605
.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 = state
.db
.get_event_by_id_for_event_write(tenant.community(), &target_id)
.await
.map_err(|e| anyhow::anyhow!("db error looking up target: {e}"))?
.ok_or_else(|| anyhow::anyhow!("target event not found"))?;
match target_event.channel_id {
Some(target_ch) if target_ch != channel_id => {
return Err(anyhow::anyhow!(
"target event belongs to a different channel"
));
}
None => {
return Err(anyhow::anyhow!("target event has no channel"));
}
_ => {} // Same channel — OK
}
// Check if actor is the event author.
// For relay-signed REST messages, the real author is in the p tag.
let author =
effective_message_author(&target_event.event, &state.relay_keypair.public_key());
if author_delete_can_use_self_delete_path(&author, &actor_bytes, event) {
// Author deleting their own message: re-gate on membership/open visibility so that
// a removed private-channel member cannot mutate old messages after access is revoked.
let is_member = state
.is_member_cached(tenant.community(), channel_id, &actor_bytes)
.await?;
if is_member {
return Ok(());
}
let is_open = stateView on GitHub (pinned to dad5a33865)