zeroclaw-labs/zeroclaw · error · anyhow::Error

Signal RPC error {code}: {msg}

Error message

Signal RPC error {code}: {msg}

What it means

Raised by SignalChannel::rpc_request when the signal-cli daemon's JSON-RPC endpoint ({http_url}/api/v1/rpc) answers with an envelope containing an 'error' object. The numeric code defaults to -1 and message to 'unknown' when the daemon omits them. Every outbound Signal operation flows through this — send_poll, send, start_typing, add_reaction, remove_reaction — so this error is the universal surface for signal-cli failures such as unregistered numbers, untrusted identities, or attachment problems.

Source

Thrown at crates/zeroclaw-channels/src/signal.rs:376

        // 201 = success with no body (e.g. typing indicators)
        if resp.status().as_u16() == 201 {
            return Ok(None);
        }

        let text = resp.text().await?;
        if text.is_empty() {
            return Ok(None);
        }

        let parsed: serde_json::Value = serde_json::from_str(&text)?;
        if let Some(err) = parsed.get("error") {
            let code = err.get("code").and_then(|c| c.as_i64()).unwrap_or(-1);
            let msg = err
                .get("message")
                .and_then(|m| m.as_str())
                .unwrap_or("unknown");
            anyhow::bail!("Signal RPC error {code}: {msg}");
        }

        Ok(parsed.get("result").cloned())
    }

    /// Process a single SSE envelope, returning one or more
    /// `ChannelMessage`s. Most envelopes produce 0 or 1 messages; a
    /// multi-select poll vote produces N (one per selected option).
    ///
    /// Inbound shape may be plain text (`dataMessage.message`) OR a
    /// poll-vote (`dataMessage.pollAnswer` or `dataMessage.pollVote`). For
    /// poll-votes we emit a synthetic message per selected option whose `content` is a
    /// documented sentinel: `"[choice-index]N"` for real signal-cli
    /// `pollVote` payloads, or `"[choice]<selected-title>"` when an
    /// alternate payload supplies titles. Consumers
    /// can match this prefix to correlate the vote with their original
    /// option set, or ignore it if they don't handle poll votes.
    ///

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Check signal-cli's own logs — the RPC message mirrors a stack trace there with the real cause
  2. Verify the recipient number in E.164 format and registered on Signal (and not the bot's own number)
  3. For untrusted-identity errors, re-verify trust for that number (trust/new-safety-number flow) or clear the broken session
  4. Confirm the daemon version supports the RPC method being called (typing, polls, reactions differ across versions)
Defensive patterns

Strategy: try-catch

Try / catch

if let Err(err) = channel.send(&recipient, &text).await {
    let msg = format!("{err:#}");
    if msg.starts_with("Signal RPC error -1") && msg.contains("Unregistered") {
        return Ok(mark_unreachable(recipient)); // permanent: number not on Signal, don't retry
    }
    if msg.starts_with("Signal RPC error") {
        tokio::time::sleep(Duration::from_secs(3)).await;
        return channel.send(&recipient, &text).await; // transient daemon hiccup: one retry
    }
    return Err(err);
}

Prevention

When it happens

Trigger: Any rpc_request(method, params) call where the parsed JSON-RPC response has .error: sending to a number not on Signal ('Unregistered user' style -1 errors), sending to the bot's own number, untrusted/changed safety numbers (identity changes), invalid recipient formats, or daemon-internal failures.

Common situations: Recipient number not E.164 or not registered on Signal; the signal-cli account's trust store missing an identity after the recipient reinstalled; signal-cli daemon version mismatch (method names like sendPollCreate or sendTyping vary by version); daemon websocket/rpc bridge misconfigured; sending to self which signal-cli rejects.

Related errors


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