block/buzz · error · IngestError::Rejected

target event belongs to a different channel

Error message

target event belongs to a different channel

What it means

Thrown by kind:9005 when the target event exists but its stored channel_id differs from the channel in the delete event's h tag. Deletes are channel-scoped: the relay looks up the target, then requires target_event.channel_id == the h-tag channel before checking authorship. This prevents cross-channel deletion via forged h tags.

Source

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

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

View on GitHub (pinned to f956e6fe06)

Solutions

  1. Resolve the target message's actual channel first (query the event by id and read its h tag / channel), then publish the 9005 with THAT channel in the h tag.
  2. In UI flows, always pair the delete with the channel the message was rendered from, not a global channel id.
  3. Refresh cached channel ids (buzz channels list) if a channel was recreated.

Example fix

// before — deleting with the wrong h tag
await sdk.deleteEvent(wrongChannelId, targetEventId); // target lives in otherChannelId

// after — use the target's own channel
const target = await buzz.messagesGet(targetEventId);
await sdk.deleteEvent(target.channelId, targetEventId);
Defensive patterns

Strategy: validation

Validate before calling

// Resolve the target's own channel before publishing the delete
const target = await buzz.messagesGet(targetEventId);
const hTag = target.tags.find((t) => t[0] === "h");
if (!hTag) throw new Error("Target is not a channel message");
if (hTag[1] !== currentChannelId) {
  currentChannelId = hTag[1]; // use the target's actual channel
}
await sdk.deleteEvent(currentChannelId, targetEventId);

Type guard

function targetMatchesChannel(targetHTag: string | undefined, deleteHTag: string): boolean {
  return targetHTag === deleteHTag;
}

Try / catch

try {
  await sdk.deleteEvent(channelId, eventId);
} catch (e) {
  if (String(e).includes("belongs to a different channel")) {
    const target = await buzz.messagesGet(eventId);
    await sdk.deleteEvent(target.tags.find((t) => t[0] === "h")![1], eventId);
  } else throw e;
}

Prevention

When it happens

Trigger: Publishing kind:9005 with h tag of channel A but an e tag pointing at a message in channel B. Commonly happens with stale cached channel ids or when a message was moved/cross-posted and the client tracks the wrong channel.

Common situations: Deep links or search results yielding a message id that the client associates with the wrong channel; channel deleted and recreated with the same name but different uuid; copy-paste of e ids between channels in tooling.

Related errors


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