block/buzz · error · anyhow::Error
invalid role: {role_str}
Error message
invalid role: {role_str} What it means
handle_put_user reads the optional `role` tag on a kind:9000 PUT_USER event and parses it into MemberRole. A present but unrecognized role string fails with 'invalid role: {role_str}'. Omitting the tag is valid: existing members keep their current role; new members default to Member (preventing silent demotion).
Source
Thrown at crates/buzz-relay/src/handlers/side_effects.rs:1306
info!(pubkey = %hex::encode(&pubkey_bytes), "kind:0 profile synced to users table");
Ok(())
}
async fn handle_put_user(
tenant: &TenantContext,
event: &Event,
state: &Arc<AppState>,
) -> anyhow::Result<()> {
let channel_id =
extract_h_tag_channel(event).ok_or_else(|| anyhow::anyhow!("missing h tag"))?;
let target_pubkey = extract_p_tag(event).ok_or_else(|| anyhow::anyhow!("missing p tag"))?;
// No role tag = no role change: preserve an existing member's current role and
// fall back to Member only for a new member. Unconditionally defaulting to
// Member let a bare PUT_USER silently demote an existing owner/admin.
let role: MemberRole = match extract_tag_value(event, "role") {
Some(role_str) => role_str
.parse()
.map_err(|_| anyhow::anyhow!("invalid role: {role_str}"))?,
None => state
.db
.get_members_for_event_write(tenant.community(), channel_id)
.await?
.iter()
.find(|m| m.pubkey == target_pubkey)
.and_then(|m| m.role.parse().ok())
.unwrap_or(MemberRole::Member),
};
let actor_bytes = event.pubkey.to_bytes().to_vec();
state
.db
.add_member(
tenant.community(),
channel_id,
&target_pubkey,View on GitHub (pinned to dad5a33865)
Solutions
- Use an exact MemberRole wire string in the `role` tag (match the enum's FromStr in buzz-db channel roles)
- Omit the `role` tag entirely if no role change is intended — existing role is preserved
- Add client-side validation against the allowed role names before publishing
- Check the buzz-db MemberRole enum for the current canonical spellings
Example fix
// before
.tag(Tag::custom("role", ["Admin"])) // capitalization mismatch
// after
.tag(Tag::custom("role", ["admin"])) // exact MemberRole parseable value Defensive patterns
Strategy: validation
Validate before calling
const ALLOWED_ROLES = ['member', 'admin', 'owner']; // match MemberRole::FromStr
const role = event.tags.find(t => t[0] === 'role')?.[1];
if (role !== undefined && !ALLOWED_ROLES.includes(role)) {
throw new Error(`invalid role: ${role}`);
} Type guard
function isValidRole(r: string | undefined): r is 'member' | 'admin' | 'owner' {
return r === undefined || ['member', 'admin', 'owner'].includes(r);
} Try / catch
match handle_put_user(...).await {
Err(e) if e.to_string().starts_with("invalid role:") => {
// surface allowed role values back to the client
}
other => other?,
} Prevention
- Derive client role literals from the buzz-db MemberRole enum, not UI labels
- Omit the role tag when no role change is intended (preserves existing role)
- Add a table test enumerating all parseable/unparseable role strings
When it happens
Trigger: Publishing a kind:9000 event whose `role` tag value is not one of MemberRole's parseable variants (e.g. 'Owner' vs 'owner', 'admin', 'moderator', typos, or capitalized forms).
Common situations: Client sends 'administrator' or 'mod' instead of the exact enum string; a UI passes a display label instead of the wire value; case mismatch after a client refactor.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- missing p tag
- moderation notice recipient must be a 32-byte pubkey, got {}
- failed to build setup nudge: {e}
- channel not found
- an admin cannot ban or time out a community owner or fellow
AI-assisted analysis of block/buzz@dad5a33865 (2026-09-05).
Data as JSON: /api/errors/9b1e94268bd6b4e5.
Report an issue: GitHub.