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
- Add ["h", "<channel-uuid>"] (plain hyphenated UUID, e.g. 550e8400-e29b-41d4-a716-446655440000) to the admin event
- Resolve the channel's UUID once from the channel list/kind 39000 metadata and reuse it for all admin events
- 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
- Resolve the channel UUID once at channel selection and thread it into every admin-event builder
- Never send channel names, bech32 ids, or event ids in the h tag — UUID only
- Add a builder-level assertion so a missing h tag fails at compile/test time in client code
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.