block/buzz · error · IngestError::Rejected

invalid visibility value: {v} (must be "open" or "private")

Error message

invalid visibility value: {v} (must be "open" or "private")

What it means

Thrown when a kind:9002 "visibility" tag value is not exactly "open" or "private". Like archived, the comparison is case-sensitive over the raw tag string, so "Open", "PUBLIC", "public", or "unlisted" are rejected.

Source

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

                    match t.content() {
                        Some(v)
                            if !buzz_core::channel::canonical_channel_name(v)
                                .trim()
                                .is_empty() => {}
                        _ => {
                            return Err(anyhow::anyhow!("channel name is required"));
                        }
                    }
                }
            }

            // Validate visibility values before storage.
            for t in event.tags.iter() {
                if t.kind().to_string() == "visibility" {
                    match t.content() {
                        Some("open") | Some("private") => {}
                        Some(v) => {
                            return Err(anyhow::anyhow!(
                                "invalid visibility value: {v} (must be \"open\" or \"private\")"
                            ));
                        }
                        None => {
                            return Err(anyhow::anyhow!("visibility tag must have a value"));
                        }
                    }
                }
            }

            // Validate ttl values before storage. Empty string clears the TTL
            // (channel becomes permanent); any other value must parse as a
            // positive integer number of seconds. A bare tag with no value is
            // rejected so clearing is always explicit (`["ttl", ""]`).
            for t in event.tags.iter() {
                if t.kind().to_string() == "ttl" {
                    match t.content() {
                        Some("") => {}

View on GitHub (pinned to f956e6fe06)

Solutions

  1. Use exactly "open" or "private" as the tag value.
  2. Map domain enums at the boundary: { internal: 'PUBLIC' } → "open".
  3. Validate with a strict allowlist before publish rather than a case-insensitive check.

Example fix

// before
{ kind: 9002, tags: [["h", ch], ["visibility", isPrivate ? "Private" : "OPEN"]] }

// after
{ kind: 9002, tags: [["h", ch], ["visibility", isPrivate ? "private" : "open"]] }
Defensive patterns

Strategy: type-guard

Validate before calling

const VISIBILITIES = ["open", "private"] as const;
type Visibility = (typeof VISIBILITIES)[number];

function visibilityTag(v: Visibility): string[] {
  return ["visibility", v]; // type rules out "Public"/"unlisted" at compile time
}

Type guard

function isVisibilityValue(v: string): v is "open" | "private" {
  return v === "open" || v === "private";
}

Try / catch

try {
  await sdk.publish(editEvent);
} catch (e) {
  if (String(e).includes("invalid visibility value")) {
    throw new Error('Visibility must be exactly "open" or "private" (lowercase)');
  } else throw e;
}

Prevention

When it happens

Trigger: Publishing [["visibility", "Public"], ["visibility", "public"], or ["visibility", "unlisted"]] in kind:9002. Only the two lowercase enum strings are accepted.

Common situations: Title-casing values for display and reusing them in the publish path; adding a third visibility tier client-side before the relay supports it; mappings from other chat systems (Slack/Discord 'private-channel' flags) converted naively.

Related errors


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