block/buzz · error · IngestError::Rejected

invalid ttl value: {v} (must be a positive integer of second

Error message

invalid ttl value: {v} (must be a positive integer of seconds, or empty to clear)

What it means

Thrown when a kind:9002 "ttl" tag value is non-empty but does not parse as a positive i32 number of seconds. Zero, negative numbers, non-numeric strings ("1h", "3600s"), decimals, and values above i32::MAX (2147483647) all fail. The empty string is the only special value — it clears the TTL.

Source

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

                        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("") => {}
                        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"

View on GitHub (pinned to f956e6fe06)

Solutions

  1. Send whole seconds as a base-10 integer string: ["ttl", "3600"] for one hour.
  2. Convert duration strings client-side (e.g. parse "24h" → 86400) before publishing.
  3. Clamp to 1..=2147483647; if you want no expiry, use the explicit clear value "" instead of 0.
  4. Double-check units — the field is seconds, not milliseconds.

Example fix

# before
buzz channels update --channel $CH --ttl "24h"   # → invalid ttl value

# after — whole seconds
buzz channels update --channel $CH --ttl 86400
# to make the channel permanent:
buzz channels update --channel $CH --ttl ""
Defensive patterns

Strategy: validation

Validate before calling

// Normalize human durations to whole seconds, clamped to i32
const MAX_TTL = 2147483647;

function ttlSeconds(input: string | number): string {
  const n = typeof input === "number" ? input : parseInt(input, 10);
  if (!Number.isInteger(n) || n <= 0 || n > MAX_TTL) {
    throw new Error(`ttl must be an integer 1..${MAX_TTL} (seconds), or "" to clear — got ${input}`);
  }
  return String(n);
}

Type guard

function isValidTtl(v: string): boolean {
  if (v === "") return true; // explicit clear
  if (!/^\d+$/.test(v)) return false;
  const n = Number(v);
  return n > 0 && n <= 2147483647;
}

Try / catch

try {
  await sdk.publish(editEvent);
} catch (e) {
  if (String(e).includes("invalid ttl value")) {
    throw new Error("TTL must be whole seconds (e.g. 86400 for 24h); use \"\" to make the channel permanent");
  } else throw e;
}

Prevention

When it happens

Trigger: Publishing [["ttl", "3600s"], ["ttl", "1h"], ["ttl", "0"], ["ttl", "-60"], ["ttl", "86400.5"], or ["ttl", "99999999999"]] in kind:9002.

Common situations: Passing human duration strings from config/env ("24h") straight into the tag; using milliseconds instead of seconds; UI computing ttl = expiry - now and getting 0 or negative for already-expired channels.

Related errors


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