block/buzz · error · IngestError::Rejected

missing or invalid h tag

Error message

missing or invalid h tag

What it means

validate_admin_event() requires every NIP-29 admin event except kind 9007 (CREATE_GROUP) to carry an 'h' tag whose content parses as a UUID — extract_h_tag_channel() returned None. The h tag is how Buzz scopes group mutations (kinds 9000, 9001, 9002, 9005, 9008, 9021, 9022) to a channel, and it must be the channel's UUID, not its name or id string from another scheme.

Source

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

    }
    Ok(false)
}

/// Validate an admin kind event BEFORE storage.
pub async fn validate_admin_event(
    tenant: &TenantContext,
    kind: u32,
    event: &Event,
    state: &Arc<AppState>,
) -> anyhow::Result<()> {
    // CREATE_GROUP doesn't need an existing channel — skip h-tag extraction
    if kind == 9007 {
        return Ok(());
    }

    // Extract channel from h tag
    let channel_id =
        extract_h_tag_channel(event).ok_or_else(|| anyhow::anyhow!("missing or invalid h tag"))?;

    let actor_bytes = event.pubkey.to_bytes().to_vec();

    // Reject mutations on archived channels — except kind:9002 with archived=false
    // (unarchive), which must be allowed through so the channel can be restored.
    let channel = state
        .db
        .get_channel(tenant.community(), channel_id)
        .await
        .map_err(|_| anyhow::anyhow!("channel not found"))?;
    let is_unarchive_request = kind == 9002
        && event.tags.iter().any(|t| {
            let parts = t.as_slice();
            parts.len() >= 2 && parts[0] == "archived" && parts[1] == "false"
        });
    if channel.archived_at.is_some() && !is_unarchive_request {
        return Err(anyhow::anyhow!("channel is archived"));
    }

View on GitHub (pinned to f956e6fe06)

Solutions

  1. Add ["h", "<channel-uuid>"] (plain hyphenated UUID, e.g. 550e8400-e29b-41d4-a716-446655440000) to the admin event
  2. Resolve the channel's UUID once from the channel list/kind 39000 metadata and reuse it for all admin events
  3. Assert client-side that the h value round-trips through Uuid::parse_str before publishing

Example fix

// before
EventBuilder::new(Kind::from(9000), "", [
    Tag::custom(TagKind::Custom("p"), vec![user_hex]),
    Tag::custom(TagKind::Custom("h"), vec!["general"]), // name, not UUID
])

// after
let channel_id: Uuid = fetch_channel("general").await?.id;
EventBuilder::new(Kind::from(9000), "", [
    Tag::custom(TagKind::Custom("p"), vec![user_hex]),
    Tag::custom(TagKind::Custom("h"), vec![channel_id.to_string()]),
])
Defensive patterns

Strategy: validation

Validate before calling

// Validate the h tag exactly like extract_h_tag_channel before publish
fn valid_h_tag(tags: &[Tag]) -> bool {
    tags.iter().any(|t| {
        t.kind().to_string() == "h"
            && t.content().and_then(|v| v.parse::<Uuid>().ok()).is_some()
    })
}
if kind != 9007 {
    assert!(valid_h_tag(&event.tags), "admin events need h = channel UUID");
}

Type guard

const isUuid = (v: string): boolean =>
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);

Try / catch

match validate_admin_event(&tenant, kind, &event, &state).await {
    Err(e) if e.to_string().contains("missing or invalid h tag") => {
        reject("admin event requires [\"h\", \"<channel-uuid>\"]", &event.id)
    }
    other => other,
}

Prevention

When it happens

Trigger: Sending PUT_USER/REMOVE_USER/edit-metadata/delete-group/join/leave without an h tag; h tag value is a channel name like "general" or a Nostr event id instead of a UUID; h tag content is a UUID with braces/quotes or wrong casing that fails Uuid::parse; tag kind serialized as uppercase 'H'.

Common situations: Clients used to NIP-28-style channels passing bech32 or name ids; UI passing the channel display name because the UUID was not threaded through state; JSON double-encoding the tag value (quotes included in the string).

Related errors


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