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
- Use exactly "open" or "private" as the tag value.
- Map domain enums at the boundary: { internal: 'PUBLIC' } → "open".
- 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
- Model visibility as a closed union type, not a free string, at every API boundary.
- Lowercase and validate external input before mapping it into event tags.
- Remember there is no third tier — reject 'unlisted'/'public' client-side with a clear message.
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
- invalid archived value: {v} (must be "true" or "false")
- kind:9002 must include at least one metadata tag (name, abou
- archived tag must have a value
- channel name is required
- visibility tag must have a value
AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16).
Data as JSON: /api/errors/f8bdd7f84cdc0fd5.
Report an issue: GitHub.