{"record":{"id":"4c0675ef7aaa649d","repo":"block/buzz","slug":"channel-name-is-required","errorCode":null,"errorMessage":"channel name is required","messagePattern":"channel name is required","errorType":"validation","errorClass":"IngestError::Rejected","httpStatus":400,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":540,"sourceCode":"                        }\n                        None => {\n                            return Err(anyhow::anyhow!(\"archived tag must have a value\"));\n                        }\n                    }\n                }\n            }\n\n            // Validate channel names before storage. A name made entirely of\n            // display-prefix hashes becomes empty after canonicalization.\n            for t in event.tags.iter() {\n                if t.kind().to_string() == \"name\" {\n                    match t.content() {\n                        Some(v)\n                            if !buzz_core::channel::canonical_channel_name(v)\n                                .trim()\n                                .is_empty() => {}\n                        _ => {\n                            return Err(anyhow::anyhow!(\"channel name is required\"));\n                        }\n                    }\n                }\n            }\n\n            // Validate visibility values before storage.\n            for t in event.tags.iter() {\n                if t.kind().to_string() == \"visibility\" {\n                    match t.content() {\n                        Some(\"open\") | Some(\"private\") => {}\n                        Some(v) => {\n                            return Err(anyhow::anyhow!(\n                                \"invalid visibility value: {v} (must be \\\"open\\\" or \\\"private\\\")\"\n                            ));\n                        }\n                        None => {\n                            return Err(anyhow::anyhow!(\"visibility tag must have a value\"));\n                        }","sourceCodeStart":522,"sourceCodeEnd":558,"githubUrl":"https://github.com/block/buzz/blob/f956e6fe06a76e50cbd8fba1a162482e752e7f1a/crates/buzz-relay/src/handlers/side_effects.rs#L522-L558","documentation":"Thrown when a kind:9002 \"name\" tag value canonicalizes to an empty string via buzz_core::channel::canonical_channel_name. This catches names made entirely of characters that are display prefixes (e.g. \"#\", \"##\") which are stripped during canonicalization, leaving nothing. The check exists so a rename can never blank out a channel name.","triggerScenarios":"Publishing [[\"name\", \"###\"], [\"name\", \"\"], [\"name\", \"#\"]], or a name consisting only of whitespace/hash prefix characters in a kind:9002 event.","commonSituations":"UI passing an untrimmed input where the user typed only '#'; pre-pending '#' to channel names client-side and then sending names like '#general' where the user typed just '#'; renaming from a template variable that rendered empty.","solutions":["Send a name containing at least one non-prefix character, e.g. [\"name\", \"general\"] without the leading '#'.","Trim and validate the input client-side: reject names whose canonical form is empty before publishing.","If the intent was to leave the name unchanged, omit the name tag from the 9002 event."],"exampleFix":"// before — UI prefixes '#' and user typed nothing but '#'\n{ kind: 9002, tags: [[\"h\", ch], [\"name\", `#${userInput}`]] } // userInput = \"#\" → \"##\"\n\n// after\nconst raw = userInput.replace(/^#+/, \"\").trim();\nif (raw) {\n  { kind: 9002, tags: [[\"h\", ch], [\"name\", raw]] };\n}","handlingStrategy":"validation","validationCode":"// Mirror the relay's canonicalization before publishing a rename\nfunction canonicalChannelName(raw: string): string {\n  return raw.replace(/^#+/, \"\").trim().toLowerCase().replace(/[\\s_]+/g, \"-\").replace(/[^a-z0-9-]/g, \"\");\n}\n\nconst canon = canonicalChannelName(nameInput);\nif (!canon) throw new Error(\"Channel name is empty after removing display prefix\");\nconst tags = [[\"h\", channelId], [\"name\", nameInput.trim()]];","typeGuard":"function isNonEmptyChannelName(raw: string): boolean {\n  // names made only of display-prefix hashes canonicalize to empty\n  return raw.replace(/^#+/, \"\").trim().length > 0;\n}","tryCatchPattern":"try {\n  await sdk.publish(editEvent);\n} catch (e) {\n  if (String(e).includes(\"channel name is required\")) {\n    form.setError(\"name\", \"Name needs at least one non-# character\");\n  } else throw e;\n}","preventionTips":["Strip any leading '#' in the UI input before building the name tag — the relay stores canonical names.","Disable the save button when the trimmed input is empty or only '#'.","Omit the name tag entirely when the name field is unchanged."],"tags":["nostr","validation","kind-9002","channel-name","normalization","relay"],"backgroundTag":"invalid-channel-name","analyzedSha":"f956e6fe06a76e50cbd8fba1a162482e752e7f1a","analyzedAt":"2026-08-16T22:11:40.750Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}