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

  1. Log the raw webhook event that produced the empty userId and identify which event type/field it came from.
  2. Gate the pairing flow earlier: skip pairing when the event's source user_id is absent or blank instead of calling persist.
  3. Fix test fixtures to carry realistic non-empty userIds.
  4. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/6f936101748c7d94. Report an issue: GitHub.