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

  1. 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).
  2. Check what the id actually is before deleting: relay query with { ids: [id] } and inspect kind and tags.
  3. If the target is channel metadata that must change, publish a replacement event of the same kind/d tag instead of a 9005 delete.
  4. 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

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


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 = state

View on GitHub (pinned to dad5a33865)