block/buzz · error · IngestError::Rejected

target event has no channel

Error message

target event has no channel

What it means

Thrown by kind:9005 when the target event exists but has channel_id = NULL in the database — it is not a channel message. The delete-by-event path only operates on channel-scoped events; direct messages, DM wrappers, and other non-channel events cannot be deleted this way.

Source

Thrown at crates/buzz-relay/src/handlers/side_effects.rs:666

                .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(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 f956e6fe06)

Solutions

  1. Only send kind:9005 for events that actually belong to a channel (they were received via a channel subscription / have an h tag).
  2. For DM deletion, use the DM-specific mechanism (kind:5 deletion event referencing the target e tag) rather than 9005.
  3. Filter candidate ids client-side: skip events whose channel association is unknown.
  4. If you expected the event to be in a channel, verify how it was published — it may have been stored outside channel scope by design.

Example fix

// before — universal delete path feeds DM ids into 9005
await sdk.deleteEvent(currentChannelId, selectedEventId); // selectedEventId is a DM → "target event has no channel"

// after — branch on event scope
if (event.tags.some((t) => t[0] === "h")) {
  await sdk.deleteEvent(channelId, event.id);          // channel message → 9005
} else {
  await sdk.publishDeletion(event.id);                 // kind:5 generic deletion
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Only route channel-scoped events into the kind:9005 path
const target = await buzz.messagesGet(eventId);
if (!target.tags.some((t) => t[0] === "h")) {
  // DM or non-channel event — use the generic deletion kind instead
  await sdk.publishDeletion(eventId);
} else {
  await sdk.deleteEvent(channelId, eventId);
}

Type guard

function isChannelEvent(tags: string[][]): boolean {
  return tags.some((t) => t[0] === "h" && typeof t[1] === "string" && t[1].length > 0);
}

Try / catch

try {
  await sdk.deleteEvent(channelId, eventId);
} catch (e) {
  if (String(e).includes("target event has no channel")) {
    await sdk.publishDeletion(eventId); // fall back to generic kind:5 deletion
  } else throw e;
}

Prevention

When it happens

Trigger: Pointing a kind:9005 e tag at a kind:14 DM/gift-wrap, a user-metadata event, or any event stored without a channel association. The h-tag scoping check finds None and rejects.

Common situations: Generic 'delete any message' UI actions that feed whatever event id is selected into the channel delete path; scripts iterating an inbox containing DMs; deletes targeting relay-signed REST artifacts recorded without channel scope.

Related errors


AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16). Data as JSON: /api/errors/33ff9f58bd442161. Report an issue: GitHub.