{"record":{"id":"e3912f80eb9fcaa1","repo":"block/buzz","slug":"ttl-tag-must-have-a-value-seconds-or-empty-strin","errorCode":null,"errorMessage":"ttl tag must have a value (seconds, or empty string to clear)","messagePattern":"ttl tag must have a value \\(seconds, or empty string to clear\\)","errorType":"validation","errorClass":"IngestError::Rejected","httpStatus":400,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":580,"sourceCode":"\n            // Validate ttl values before storage. Empty string clears the TTL\n            // (channel becomes permanent); any other value must parse as a\n            // positive integer number of seconds. A bare tag with no value is\n            // rejected so clearing is always explicit (`[\"ttl\", \"\"]`).\n            for t in event.tags.iter() {\n                if t.kind().to_string() == \"ttl\" {\n                    match t.content() {\n                        Some(\"\") => {}\n                        Some(v) => match v.parse::<i32>() {\n                            Ok(n) if n > 0 => {}\n                            _ => {\n                                return Err(anyhow::anyhow!(\n                                    \"invalid ttl value: {v} (must be a positive integer of seconds, or empty to clear)\"\n                                ));\n                            }\n                        },\n                        None => {\n                            return Err(anyhow::anyhow!(\n                                \"ttl tag must have a value (seconds, or empty string to clear)\"\n                            ));\n                        }\n                    }\n                }\n            }\n\n            // name/about/archived/visibility/ttl require owner/admin;\n            // topic/purpose allow any member.\n            let has_privileged_tag = event.tags.iter().any(|t| {\n                let k = t.kind().to_string();\n                k == \"name\" || k == \"about\" || k == \"archived\" || k == \"visibility\" || k == \"ttl\"\n            });\n            if has_privileged_tag {\n                let members = state.db.get_members(tenant.community(), channel_id).await?;\n                let actor_member = members.iter().find(|m| m.pubkey == actor_bytes);\n                match actor_member {\n                    Some(m) if m.role == \"owner\" || m.role == \"admin\" => Ok(()),","sourceCodeStart":562,"sourceCodeEnd":598,"githubUrl":"https://github.com/block/buzz/blob/f956e6fe06a76e50cbd8fba1a162482e752e7f1a/crates/buzz-relay/src/handlers/side_effects.rs#L562-L598","documentation":"Thrown when a kind:9002 event carries a bare [\"ttl\"] tag with no value. The relay rejects this deliberately: clearing the TTL must be an explicit [\"ttl\", \"\"] so that a value-less tag can never be mistaken for 'make permanent'.","triggerScenarios":"Publishing [[\"ttl\"] as a single-element tag, or an encoding path that strips empty strings from tag values before send.","commonSituations":"Serialization layers that drop empty-string tag values (JSON→Nostr tag mappers); clients sending a null ttl hoping to unset it; generic tag builders that omit falsy values.","solutions":["To clear the TTL, send the explicit empty string: [\"ttl\", \"\"].","To set a TTL, send seconds: [\"ttl\", \"3600\"].","Fix any tag serializer that filters out empty strings; the empty value is meaningful here.","If TTL should stay unchanged, omit the ttl tag entirely."],"exampleFix":"// before — serializer dropped the empty value\n{ kind: 9002, tags: [[\"h\", ch], [\"ttl\"]] } // was meant to clear TTL\n\n// after — explicit empty string survives serialization\nconst tags = [[\"h\", ch], [\"ttl\", \"\"]]; // build literal pairs; do not filter empty values","handlingStrategy":"validation","validationCode":"// The empty-string value is meaningful — never filter it out\nfunction ttlTag(seconds: number | null): string[] {\n  return [\"ttl\", seconds === null ? \"\" : String(seconds)];\n}","typeGuard":"function isWellFormedTtlTag(t: string[]): boolean {\n  // must have exactly the value slot; only \"\" or a positive-integer string is valid\n  return t.length >= 2 && (t[1] === \"\" || /^\\d+$/.test(t[1]));\n}","tryCatchPattern":"try {\n  await sdk.publish(editEvent);\n} catch (e) {\n  if (String(e).includes(\"ttl tag must have a value\")) {\n    throw new Error('Use [\"ttl\", \"\"] to clear or [\"ttl\", \"3600\"] to set — bare [\"ttl\"] is rejected');\n  } else throw e;\n}","preventionTips":["Audit tag serializers for empty-string filtering (e.g. omitting falsy values) — it silently breaks TTL clears.","Represent 'permanent' as null in your model and encode it as the explicit empty string.","Leaving the ttl tag out entirely means 'unchanged', not 'cleared' — keep the three states distinct."],"tags":["nostr","validation","kind-9002","ttl","missing-tag-value","relay"],"backgroundTag":"missing-tag-value","analyzedSha":"f956e6fe06a76e50cbd8fba1a162482e752e7f1a","analyzedAt":"2026-08-16T22:11:40.750Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}