{"record":{"id":"6f936101748c7d94","repo":"zeroclaw-labs/zeroclaw","slug":"cannot-persist-empty-line-userid","errorCode":null,"errorMessage":"Cannot persist empty LINE userId","messagePattern":"Cannot persist empty LINE userId","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/line.rs","lineNumber":251,"sourceCode":"/// No-op-with-warn when `state.persist` is unset (test fixtures).\nasync fn persist_line_paired_identity(state: &LineState, user_id: &str) -> anyhow::Result<()> {\n    use anyhow::Context;\n    use zeroclaw_config::multi_agent::{PeerGroupConfig, PeerUsername};\n    use zeroclaw_config::providers::ChannelRef;\n\n    let Some(config) = &state.persist else {\n        ::zeroclaw_log::record!(\n            WARN,\n            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)\n                .with_outcome(::zeroclaw_log::EventOutcome::Unknown)\n                .with_attrs(::serde_json::json!({\"user_id\": user_id})),\n            \"paired userId not persisted (no persistence handle wired)\"\n        );\n        return Ok(());\n    };\n    let normalized = user_id.trim().to_string();\n    if normalized.is_empty() {\n        anyhow::bail!(\"Cannot persist empty LINE userId\");\n    }\n    let group_name = format!(\"line_{}\", state.alias);\n    let channel_ref = ChannelRef::new(format!(\"line.{}\", state.alias));\n    let snapshot = {\n        let mut cfg = config.write();\n        if !cfg.channels.line.contains_key(&state.alias) {\n            anyhow::bail!(\"Missing [channels.line.{}] section\", state.alias);\n        }\n        let group = cfg\n            .peer_groups\n            .entry(group_name)\n            .or_insert_with(|| PeerGroupConfig {\n                channel: channel_ref,\n                ..PeerGroupConfig::default()\n            });\n        if group\n            .external_peers\n            .iter()","sourceCodeStart":233,"sourceCodeEnd":269,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/line.rs#L233-L269","documentation":"persist_line_paired_identity trims the incoming LINE userId and bails if the result is empty before writing the paired identity into the live config. Reaching this error means a webhook event reached the pairing flow with a blank or whitespace-only userId — a data-quality problem in the event (or its parsing), since real LINE user-source events always carry a userId. This is a validation guard, so the config is never mutated when it fires.","triggerScenarios":"Crafted or replayed webhook fixtures with an empty source.user_id, test events from the LINE console, a schema/API change dropping the field, or upstream code passing an unparsed string.","commonSituations":"Developers replaying recorded webhook bodies for testing; integration fixtures with placeholder user IDs; changes to LINE's event schema leaving userId unextracted.","solutions":["Log the raw webhook event that produced the empty userId and identify which event type/field it came from.","Gate the pairing flow earlier: skip pairing when the event's source user_id is absent or blank instead of calling persist.","Fix test fixtures to carry realistic non-empty userIds.","Keep the guard — it correctly prevents corrupting config with an empty identity."],"exampleFix":"// before (caller)\nstate.call_persist_line_paired_identity(&user_id).await?;\n\n// after (caller validates first)\nif user_id.trim().is_empty() {\n    tracing::warn!(\"skipping pairing: event has empty LINE userId\");\n    return Ok(());\n}\nstate.call_persist_line_paired_identity(&user_id).await?;","handlingStrategy":"validation","validationCode":"// Gate pairing before calling the persist API\nlet user_id = event_source_user_id(&webhook_event)\n    .context(\"event carries no source.user_id\")?;\nanyhow::ensure!(!user_id.trim().is_empty(), \"empty LINE userId; refusing to pair\");","typeGuard":"fn has_valid_line_user_id(ev: &serde_json::Value) -> bool {\n    ev.pointer(\"/source/userId\")\n        .and_then(|v| v.as_str())\n        .map(|s| !s.trim().is_empty())\n        .unwrap_or(false)\n}","tryCatchPattern":"if let Err(e) = persist_line_paired_identity(&user_id).await {\n    if e.to_string().contains(\"empty LINE userId\") {\n        tracing::warn!(\"dropping pairing event with blank userId\"); // data issue, not a crash\n        return Ok(());\n    }\n    return Err(e);\n}","preventionTips":["Validate webhook payloads (signature, userId presence) before acting on them.","Keep test fixtures realistic: real-shaped userIds in replayed events.","Never write identities derived from unparsed fields."],"tags":["line","webhook","validation","user-id","pairing"],"backgroundTag":"missing-required-field","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}