zeroclaw-labs/zeroclaw · error · anyhow::Error
Cannot persist empty LINE userId
Error message
Cannot persist empty LINE userId
What it means
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.
Source
Thrown at crates/zeroclaw-channels/src/line.rs:251
/// No-op-with-warn when `state.persist` is unset (test fixtures).
async fn persist_line_paired_identity(state: &LineState, user_id: &str) -> anyhow::Result<()> {
use anyhow::Context;
use zeroclaw_config::multi_agent::{PeerGroupConfig, PeerUsername};
use zeroclaw_config::providers::ChannelRef;
let Some(config) = &state.persist else {
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
.with_outcome(::zeroclaw_log::EventOutcome::Unknown)
.with_attrs(::serde_json::json!({"user_id": user_id})),
"paired userId not persisted (no persistence handle wired)"
);
return Ok(());
};
let normalized = user_id.trim().to_string();
if normalized.is_empty() {
anyhow::bail!("Cannot persist empty LINE userId");
}
let group_name = format!("line_{}", state.alias);
let channel_ref = ChannelRef::new(format!("line.{}", state.alias));
let snapshot = {
let mut cfg = config.write();
if !cfg.channels.line.contains_key(&state.alias) {
anyhow::bail!("Missing [channels.line.{}] section", state.alias);
}
let group = cfg
.peer_groups
.entry(group_name)
.or_insert_with(|| PeerGroupConfig {
channel: channel_ref,
..PeerGroupConfig::default()
});
if group
.external_peers
.iter()View on GitHub (pinned to 88bb9c8533)
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.
Example fix
// before (caller)
state.call_persist_line_paired_identity(&user_id).await?;
// after (caller validates first)
if user_id.trim().is_empty() {
tracing::warn!("skipping pairing: event has empty LINE userId");
return Ok(());
}
state.call_persist_line_paired_identity(&user_id).await?; Defensive patterns
Strategy: validation
Validate before calling
// Gate pairing before calling the persist API
let user_id = event_source_user_id(&webhook_event)
.context("event carries no source.user_id")?;
anyhow::ensure!(!user_id.trim().is_empty(), "empty LINE userId; refusing to pair"); Type guard
fn has_valid_line_user_id(ev: &serde_json::Value) -> bool {
ev.pointer("/source/userId")
.and_then(|v| v.as_str())
.map(|s| !s.trim().is_empty())
.unwrap_or(false)
} Try / catch
if let Err(e) = persist_line_paired_identity(&user_id).await {
if e.to_string().contains("empty LINE userId") {
tracing::warn!("dropping pairing event with blank userId"); // data issue, not a crash
return Ok(());
}
return Err(e);
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: Developers replaying recorded webhook bodies for testing; integration fixtures with placeholder user IDs; changes to LINE's event schema leaving userId unextracted.
Understand the failure class
Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.
Related errors
- Cannot persist empty Telegram identity
- webhook reply failed ({status}): {err}
- modal custom_id exceeds Discord's 100-char limit; cannot ope
- interaction followup edit failed ({status}): {err}
- slash command registration failed for '{name}' ({status}): {
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/6f936101748c7d94.
Report an issue: GitHub.