{"record":{"id":"4720b658f242db49","repo":"block/buzz","slug":"invalid-ttl-value-v-must-be-a-positive-integer","errorCode":null,"errorMessage":"invalid ttl value: {v} (must be a positive integer of seconds, or empty to clear)","messagePattern":"invalid ttl value: (.+?) \\(must be a positive integer of seconds, or empty to clear\\)","errorType":"validation","errorClass":"IngestError::Rejected","httpStatus":400,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":574,"sourceCode":"                        None => {\n                            return Err(anyhow::anyhow!(\"visibility tag must have a value\"));\n                        }\n                    }\n                }\n            }\n\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\"","sourceCodeStart":556,"sourceCodeEnd":592,"githubUrl":"https://github.com/block/buzz/blob/f956e6fe06a76e50cbd8fba1a162482e752e7f1a/crates/buzz-relay/src/handlers/side_effects.rs#L556-L592","documentation":"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.","triggerScenarios":"Publishing [[\"ttl\", \"3600s\"], [\"ttl\", \"1h\"], [\"ttl\", \"0\"], [\"ttl\", \"-60\"], [\"ttl\", \"86400.5\"], or [\"ttl\", \"99999999999\"]] in kind:9002.","commonSituations":"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.","solutions":["Send whole seconds as a base-10 integer string: [\"ttl\", \"3600\"] for one hour.","Convert duration strings client-side (e.g. parse \"24h\" → 86400) before publishing.","Clamp to 1..=2147483647; if you want no expiry, use the explicit clear value \"\" instead of 0.","Double-check units — the field is seconds, not milliseconds."],"exampleFix":"# before\nbuzz channels update --channel $CH --ttl \"24h\"   # → invalid ttl value\n\n# after — whole seconds\nbuzz channels update --channel $CH --ttl 86400\n# to make the channel permanent:\nbuzz channels update --channel $CH --ttl \"\"","handlingStrategy":"validation","validationCode":"// Normalize human durations to whole seconds, clamped to i32\nconst MAX_TTL = 2147483647;\n\nfunction ttlSeconds(input: string | number): string {\n  const n = typeof input === \"number\" ? input : parseInt(input, 10);\n  if (!Number.isInteger(n) || n <= 0 || n > MAX_TTL) {\n    throw new Error(`ttl must be an integer 1..${MAX_TTL} (seconds), or \"\" to clear — got ${input}`);\n  }\n  return String(n);\n}","typeGuard":"function isValidTtl(v: string): boolean {\n  if (v === \"\") return true; // explicit clear\n  if (!/^\\d+$/.test(v)) return false;\n  const n = Number(v);\n  return n > 0 && n <= 2147483647;\n}","tryCatchPattern":"try {\n  await sdk.publish(editEvent);\n} catch (e) {\n  if (String(e).includes(\"invalid ttl value\")) {\n    throw new Error(\"TTL must be whole seconds (e.g. 86400 for 24h); use \\\"\\\" to make the channel permanent\");\n  } else throw e;\n}","preventionTips":["Always convert duration strings (\"24h\") and millisecond values to seconds before publishing.","Clamp the computed ttl = expiry - now to at least 1, or send \"\" when the channel should be permanent.","Unit-test the ttl encoder with 0, negative, \"1h\", and overflow values."],"tags":["nostr","validation","kind-9002","ttl","duration-format","relay"],"backgroundTag":"invalid-duration-format","analyzedSha":"f956e6fe06a76e50cbd8fba1a162482e752e7f1a","analyzedAt":"2026-08-16T22:11:40.750Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}