block/buzz · error · IngestError::Rejected
ttl tag must have a value (seconds, or empty string to clear
Error message
ttl tag must have a value (seconds, or empty string to clear)
What it means
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'.
Source
Thrown at crates/buzz-relay/src/handlers/side_effects.rs:580
// 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("") => {}
Some(v) => match v.parse::<i32>() {
Ok(n) if n > 0 => {}
_ => {
return Err(anyhow::anyhow!(
"invalid ttl value: {v} (must be a positive integer of seconds, or empty to clear)"
));
}
},
None => {
return Err(anyhow::anyhow!(
"ttl tag must have a value (seconds, or empty string to clear)"
));
}
}
}
}
// name/about/archived/visibility/ttl require owner/admin;
// topic/purpose allow any member.
let has_privileged_tag = event.tags.iter().any(|t| {
let k = t.kind().to_string();
k == "name" || k == "about" || k == "archived" || k == "visibility" || k == "ttl"
});
if has_privileged_tag {
let members = state.db.get_members(tenant.community(), channel_id).await?;
let actor_member = members.iter().find(|m| m.pubkey == actor_bytes);
match actor_member {
Some(m) if m.role == "owner" || m.role == "admin" => Ok(()),View on GitHub (pinned to f956e6fe06)
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.
Example fix
// before — serializer dropped the empty value
{ kind: 9002, tags: [["h", ch], ["ttl"]] } // was meant to clear TTL
// after — explicit empty string survives serialization
const tags = [["h", ch], ["ttl", ""]]; // build literal pairs; do not filter empty values Defensive patterns
Strategy: validation
Validate before calling
// The empty-string value is meaningful — never filter it out
function ttlTag(seconds: number | null): string[] {
return ["ttl", seconds === null ? "" : String(seconds)];
} Type guard
function isWellFormedTtlTag(t: string[]): boolean {
// must have exactly the value slot; only "" or a positive-integer string is valid
return t.length >= 2 && (t[1] === "" || /^\d+$/.test(t[1]));
} Try / catch
try {
await sdk.publish(editEvent);
} catch (e) {
if (String(e).includes("ttl tag must have a value")) {
throw new Error('Use ["ttl", ""] to clear or ["ttl", "3600"] to set — bare ["ttl"] is rejected');
} else throw e;
} Prevention
- 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.
When it happens
Trigger: Publishing [["ttl"] as a single-element tag, or an encoding path that strips empty strings from tag values before send.
Common situations: 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.
Related errors
- archived tag must have a value
- visibility tag must have a value
- invalid ttl value: {v} (must be a positive integer of second
- kind:9002 must include at least one metadata tag (name, abou
- invalid archived value: {v} (must be "true" or "false")
AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16).
Data as JSON: /api/errors/e3912f80eb9fcaa1.
Report an issue: GitHub.