Skip to content

refactor(errors): typed ConnectError and friends for the public API - #1090

Merged
jlucaso1 merged 4 commits into
mainfrom
claude/typed-errors-public-api-yplull
Jul 24, 2026
Merged

jlucaso1 merged 4 commits into
mainfrom
claude/typed-errors-public-api-yplull

Conversation

@jlucaso1

Copy link
Copy Markdown
Collaborator

Summary

A handful of public entry points still returned bare anyhow::Error even though the crate already models its failures with thiserror, so callers had nothing to match on but strings, and one signature promised a failure it could never produce. This replaces those with typed errors on the public surface and tightens two enums that were missing #[non_exhaustive]. Pre-1.0, so the breaking signature changes are taken now rather than carried forward.

Changes

  • ConnectError / ConnectStage (breaking): Client::connect(), wait_for_socket() and wait_for_connected() now return Result<(), ConnectError>. A timeout is a single Timeout { stage, timeout } variant rather than four look-alike variants, so a caller can answer "did it time out, and where?" in one match instead of parsing a message. Handshake failures stay typed via #[from] HandshakeError, which is what the reconnect loop already classifies on.
  • ClientError::AlreadyConnected removed (breaking): the case now lives on ConnectError, and the old variant had no remaining constructor.
  • logout() is infallible (breaking): the body only ever returned Ok(()). The deregistration IQ is best-effort (it cannot be sent at all while offline) and the local teardown runs either way, so there was nothing for a caller to branch on. Signature now matches the behavior; a failed IQ is still logged at warn.
  • MessageEditError (breaking): EncryptedEdit::original_sender_jid, SecretEncrypted::original_sender_jid and original_sender_for_dispatch return it instead of anyhow::Error. The type is re-exported from the crate root, so its failures should be inspectable; the two real modes are a malformed JID in the target key and a key carrying neither participant nor remoteJid.
  • SignalMaintenanceError (breaking): flush_pending_signal_state() and the signed pre-key rotation path return it. It separates unusable key material (CorruptKey, where a retry reads back the same bytes) from storage, IQ and drain failures, which is exactly the distinction a caller needs to decide whether retrying helps. Storage causes are wrapped with anyhow::Error::new(e).context(..) so the typed backend error stays in the source() chain.
  • Noise error no longer flattened: noise_socket.rs passed anyhow!(e.to_string()) into EncryptSendError::crypto, discarding the NoiseError type and the whole source() chain. It now passes e directly, which the constructor already accepted.
  • #[non_exhaustive] on PasskeyError and EncryptSendError: the last public error types without it. EncryptSendError is a struct with public fields, so construction goes through the existing constructors.

Tests cover both directions for each new behavior: the timeout and the already-connected rejection on connect, teardown with a failing and a non-sendable deregistration IQ, a healthy and an injected-failure signal flush, a corrupt staged pre-key and a disconnected rotation, both edit-target failure modes, and a downcast asserting the NoiseError survives inside EncryptSendError.

Validation

  • cargo fmt --all --check
  • cargo clippy -p whatsapp-rust -p wacore --all-targets -- -D warnings (workspace-wide clippy is not runnable in my environment: the voip-cli example pulls in alsa-sys, whose build script fails without ALSA headers. The other members were covered with cargo check on e2e-tests, bench-integration, chat-store and plugin-metrics, all clean.)
  • cargo test -p whatsapp-rust --lib — 1150 passed, 0 failed, 1 ignored

Full matrix, including wasm32 and the e2e suite, left to CI.


Generated by Claude Code

Several public entry points still returned bare anyhow::Error even though
the crate already models its failures with thiserror. Callers could only
match on strings, and one signature promised a failure it never produced.

- ConnectError/ConnectStage replace anyhow on connect(), wait_for_socket()
  and wait_for_connected(), so a timeout is matchable and says which step
  of the flow expired instead of being just another opaque message.
- logout() no longer returns Result: the deregistration IQ is best-effort
  and the local teardown runs either way, so there was nothing to branch on.
- MessageEditError types the JID resolution failures of a type that is
  re-exported from the crate root.
- SignalMaintenanceError separates unusable key material from storage, IQ
  and drain failures, which decides whether a retry can help.
- The noise socket now passes NoiseError straight into EncryptSendError
  instead of stringifying it, keeping the source chain intact.
- PasskeyError and EncryptSendError gain #[non_exhaustive] so later
  variants and fields are not breaking; ClientError::AlreadyConnected is
  dropped now that ConnectError owns that case.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added structured connection errors with connect stages (socket/readiness/timeout/transport/version) and exposed them via the public API.
    • Introduced typed signal-maintenance and message-edit errors, with updated public exposure.
  • Bug Fixes

    • Signal flush/maintenance failures are now surfaced as typed errors instead of being skipped or wrapped generically.
    • Logout is now infallible while keeping teardown behavior.
    • Improved error detail preservation for encryption and protocol/signal error conversions.
  • Chores

    • Marked selected error types as non-exhaustive for forward-compatible evolution.

Walkthrough

The PR introduces typed public errors for client connection, Signal maintenance, and message-edit sender resolution. It updates lifecycle and rotation APIs, preserves encryption error sources, adds public re-exports, marks selected errors non-exhaustive, and adjusts tests.

Changes

Typed client lifecycle

Layer / File(s) Summary
Connection lifecycle errors
src/client.rs, src/client/lifecycle.rs, src/client/extension_lifecycle.rs, src/lib.rs
ConnectError and ConnectStage replace generic connection errors, timeout stages are explicit, logout() is infallible, and the new types are re-exported publicly.

Signal maintenance

Layer / File(s) Summary
Signal state and key rotation errors
src/client/adapters.rs, src/features/rotate_key.rs, src/features/signal.rs, src/signal_flush.rs
Signal cache flushing and signed pre-key rotation return SignalMaintenanceError, with typed storage, drain, IQ, protocol, and corrupt-key mappings.

Message edit resolution

Layer / File(s) Summary
MESSAGE_EDIT sender errors
src/features/message_edit.rs, src/features/mod.rs, src/lib.rs
Sender resolution returns MessageEditError for invalid JIDs or missing sender fields, with public re-exports and updated tests.

Error contract hardening

Layer / File(s) Summary
Error source and exhaustiveness updates
src/socket/error.rs, src/socket/noise_socket.rs, src/passkey/mod.rs
Encryption failures preserve concrete sources, and EncryptSendError and PasskeyError are marked non-exhaustive.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant connect_graph
  participant ReadinessWait
  Client->>connect_graph: connect()
  connect_graph->>ReadinessWait: wait_for_socket() / wait_for_connected()
  ReadinessWait-->>connect_graph: ConnectError::Timeout with stage
  connect_graph-->>Client: connection result
Loading

Possibly related PRs

Suggested labels: api-design, breaking-change

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: moving the public API to typed error types.
Description check ✅ Passed The description matches the changeset and explains the typed error refactor, breaking API changes, and validation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/typed-errors-public-api-yplull

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 9.89 MiB 9.89 MiB +1.25 KiB (+0.01%) 🔺
bin .text 7.94 MiB 7.94 MiB +1.69 KiB (+0.02%) 🔺
bin allocated (text+data+bss) 9.88 MiB 9.88 MiB -160 B (-0.00%) 🔽
llvm-lines wacore 490,362 490,362 0
llvm-lines wacore copies 16,315 16,315 0
llvm-lines whatsapp-rust lib 686,903 688,104 +1,201 (+0.17%) 🔺
llvm-lines whatsapp-rust lib copies 21,899 21,941 +42 (+0.19%) 🔺
deps crates (Cargo.lock) 471 471 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.74 MiB 1.74 MiB -735 B (-0.04%) 🔽
.text wacore 654.02 KiB 652.73 KiB -1.29 KiB (-0.20%) 🔽
.text wacore_binary 89.42 KiB 89.42 KiB 0
.text wacore_libsignal 161.89 KiB 161.89 KiB 0
.text wacore_appstate 22.36 KiB 22.36 KiB 0
.text wacore_noise 22.98 KiB 21.60 KiB -1.37 KiB (-5.98%) 🎉
.text waproto 1.74 MiB 1.74 MiB 0
.text whatsapp_rust_sqlite_storage 510.95 KiB 510.95 KiB 0
.text whatsapp_rust_tokio_transport 39.78 KiB 39.78 KiB 0
.text whatsapp_rust_ureq_http_client 10.36 KiB 10.36 KiB 0
.text std 1.06 MiB 1.06 MiB +1.25 KiB (+0.11%) 🔺
.text other deps 1.88 MiB 1.88 MiB +3.96 KiB (+0.21%) 🔺
Top movers (cargo-bloat attribution)
Crate main PR Δ
metrics_exporter_prometheus (absent) 3.96 KiB +3.96 KiB
wacore_noise 22.98 KiB 21.60 KiB -1.37 KiB (-5.98%)
wacore 654.02 KiB 652.73 KiB -1.29 KiB (-0.20%)
std 1.06 MiB 1.06 MiB +1.25 KiB (+0.11%)

Baseline: d33426045 (latest main run) · Head: 221b9fdfb · Graphs

@greptile-apps

greptile-apps Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR replaces bare anyhow::Error returns across the public API with typed error enums (ConnectError, SignalMaintenanceError, MessageEditError), makes logout() infallible to match its actual behavior, fixes the NoiseError type being flattened to a string, and tightens PasskeyError/EncryptSendError with #[non_exhaustive]. These are intentional breaking changes ahead of 1.0.

  • ConnectError / ConnectStage: connect(), wait_for_socket(), wait_for_connected() all return Result<(), ConnectError>. The old four look-alike timeout variants collapse into one Timeout { stage, timeout } struct variant. The reconnect loop now matches typed variants instead of downcasting through anyhow.
  • SignalMaintenanceError: flush_pending_signal_state() and rotate_signed_pre_key() return it. The key distinction — CorruptKey (same bytes on retry, no point retrying) vs. storage/IQ/drain failures (worth retrying) — is now structurally enforced. The storage_err helper wraps backend errors as Storage(anyhow::Error::new(e).context(...)), keeping the typed backend cause in the source() chain.
  • MessageEditError and logout() infallibility: Both bring signatures into line with actual behavior; the deregistration IQ failure is logged at warn rather than surfaced.

Confidence Score: 5/5

Safe to merge. The changes are purely additive on the type layer and correctly narrow every public return type from bare anyhow::Error to a typed enum.

All breaking changes are intentional and scoped pre-1.0. The reconnect loop, internal flush paths, and Signal maintenance paths were checked to confirm the new types compose correctly across crate boundaries. The storage_err helper preserves typed causes in the source() chain. The From for SignalError impl correctly routes Signal primitives to Protocol and wraps everything else as Internal. Tests cover both directions for every new behavior. No logic was changed, only return types.

No files require special attention.

Important Files Changed

Filename Overview
src/client.rs Introduces ConnectError, ConnectStage, and SignalMaintenanceError public error types; removes ClientError::AlreadyConnected. Enum definitions are clean, doc-comments are clear, and #[non_exhaustive] is correctly applied.
src/client/lifecycle.rs connect(), connect_graph(), wait_for_socket(), wait_for_connected() return ConnectError; logout() is now infallible. Reconnect loop transitions from anyhow downcast to typed pattern match on ConnectError::Handshake. Tests cover all branches.
src/client/adapters.rs flush_pending_signal_state() and flush_signal_cache_batch_safe() now return SignalMaintenanceError. persist_signal_state_pre_wire() (pub(crate)) converts via Into::into for the anyhow::Error return path. Tests confirm healthy and failure paths.
src/features/rotate_key.rs All key-rotation paths now return SignalMaintenanceError. CorruptKey correctly covers decode/field failures on staged records; Signal covers signing-backend failures; storage_err helper preserves typed cause in source() chain.
src/features/message_edit.rs New MessageEditError with InvalidTargetJid and MissingTargetSender; original_sender_jid and original_sender_for_dispatch return it. Tests verify all three failure modes and confirm source() chain is preserved.
src/features/signal.rs From for SignalError implemented: Signal variant routes to Protocol, everything else wraps into Internal. Correctly matches within the same crate despite #[non_exhaustive].
src/socket/noise_socket.rs NoiseError no longer flattened to anyhow!(e.to_string()); passed directly to EncryptSendError::crypto(). A test asserts NoiseError downcasts correctly from the source() chain.
src/socket/error.rs EncryptSendError gains #[non_exhaustive]; struct literal construction outside the crate is now rejected while field access remains. Tests cover NoiseError and opaque source preservation.
src/lib.rs ConnectError/ConnectStage added to prelude; ConnectError/ConnectStage/SignalMaintenanceError/MessageEditError all re-exported at the crate root. Consistent with how sibling error types are handled.
src/signal_flush.rs coalesced_flush_attempt() (pub(crate), returns anyhow::Error) converts SignalMaintenanceError via Into::into — correct for an internal path that predates the typed surface.
src/passkey/mod.rs PasskeyError gains #[non_exhaustive] — minimal, correct change.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Client::connect()"] -->|"ConnectError::NotActivated"| ERR1["lifecycle not active"]
    A -->|"ConnectError::AlreadyConnected"| ERR2["already connecting / connected"]
    A --> B["connect_graph()"]
    B -->|"ConnectError::Timeout { stage: VersionFetch }"| ERR3["version fetch timed out"]
    B -->|"ConnectError::Version(...)"| ERR4["version resolve failed"]
    B -->|"ConnectError::Timeout { stage: Transport }"| ERR5["transport timed out"]
    B -->|"ConnectError::Transport(...)"| ERR6["transport open failed"]
    B --> C["do_handshake()"]
    C -->|"ConnectError::Handshake(HandshakeError)"| ERR7["noise handshake failed"]
    C --> D["Connected ✓"]
    D --> E["wait_for_socket()"]
    E -->|"ConnectError::Timeout { stage: Socket }"| ERR8["socket wait timed out"]
    D --> F["wait_for_connected()"]
    F -->|"ConnectError::Timeout { stage: Ready }"| ERR9["ready wait timed out"]
    G["flush_pending_signal_state() / rotate_signed_pre_key()"] -->|"SignalMaintenanceError::CorruptKey"| ERR10["retry won't help"]
    G -->|"SignalMaintenanceError::Storage"| ERR11["backend failure - retryable"]
    G -->|"SignalMaintenanceError::Iq"| ERR12["server rejected - retryable"]
    G -->|"SignalMaintenanceError::Signal"| ERR13["signing primitive failed"]
    G -->|"SignalMaintenanceError::DrainCommitFailed / DrainShuttingDown"| ERR14["drain safety guard"]
Loading

Reviews (4): Last reviewed commit: "refactor(errors): dedupe storage mapping..." | Re-trigger Greptile

Comment thread src/lib.rs
Comment thread src/lib.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/lib.rs (1)

202-208: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Ship the whole family — SignalMaintenanceError is missing from the prelude.

We just added ConnectError/ConnectStage to prelude for exactly this reason (letting prelude::* users match on the new typed errors), but SignalMaintenanceError — the return type of two other new-in-this-PR public methods (rotate_signed_pre_key, flush_pending_signal_state) — didn't get the same treatment. If we're doing this, let's do it consistently for every consumer-facing type from the same module.

🔧 Proposed fix
-    pub use crate::client::{ConnectError, ConnectStage};
+    pub use crate::client::{ConnectError, ConnectStage, SignalMaintenanceError};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib.rs` around lines 202 - 208, Update the crate prelude module to
re-export SignalMaintenanceError alongside the existing client error types, so
users of prelude::* can access the return type of rotate_signed_pre_key and
flush_pending_signal_state consistently with ConnectError and ConnectStage.
src/client/lifecycle.rs (1)

466-476: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stop the connect-failure logs from hiding the root cause.

In {#}, anyhow prints its full cause chain, but ConnectError::Version / ConnectError::Transport here render only the static message. The same Debug-formatted pattern used in src/client/adapters.rs’s Signal flush errors should be used here so on-call can see the underlying connection/version error.

🔧 Proposed fix
                 if is_transient {
-                    debug!("Transient connect failure, will retry: {connect_err:#}");
+                    debug!("Transient connect failure, will retry: {connect_err:?}");
                 } else {
-                    error!("Failed to connect: {connect_err:#}. Will retry...");
+                    error!("Failed to connect: {connect_err:?}. Will retry...");
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/lifecycle.rs` around lines 466 - 476, Update the connect failure
logging in the lifecycle retry path around self.connect().await to use the same
Debug-formatted error pattern as the Signal flush errors in adapters.rs,
ensuring ConnectError::Version and ConnectError::Transport include their
underlying causes while preserving the transient and non-transient log branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/message_edit.rs`:
- Around line 50-53: Update the documentation comment for the
MissingTargetSender error variant to state that from_me was not Some(true),
covering both None and Some(false), while preserving the existing error behavior
and message.
- Around line 228-249: Add a test covering the resolve_target_sender path with
no participant, from_me set to Some(false), and a malformed remote_jid; assert
the returned InvalidTargetJid contains field "remoteJid" and preserves the
original parse error source.
- Around line 674-734: The test fixtures in
original_sender_jid_reports_an_unparseable_participant and
original_sender_jid_reports_a_target_key_without_any_sender use nonconforming
phone-like JID literals. Replace the my_jid values with repository-compliant
fictitious NANP data using a real NPA, the 555 exchange, and a 0100–0199 line
number, while preserving the existing test behavior.

In `@src/features/rotate_key.rs`:
- Around line 66-70: In the rotate-key function, replace the repeated Storage
error mapping at each backend call site with a shared storage_err helper that
accepts the static context and returns the SignalMaintenanceError mapping.
Update all listed sites, including the dynamic new_id context by constructing it
before the call, while preserving each existing context message.

---

Outside diff comments:
In `@src/client/lifecycle.rs`:
- Around line 466-476: Update the connect failure logging in the lifecycle retry
path around self.connect().await to use the same Debug-formatted error pattern
as the Signal flush errors in adapters.rs, ensuring ConnectError::Version and
ConnectError::Transport include their underlying causes while preserving the
transient and non-transient log branches.

In `@src/lib.rs`:
- Around line 202-208: Update the crate prelude module to re-export
SignalMaintenanceError alongside the existing client error types, so users of
prelude::* can access the return type of rotate_signed_pre_key and
flush_pending_signal_state consistently with ConnectError and ConnectStage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f936e80d-68e9-4145-a791-5eb4cbf0d46f

📥 Commits

Reviewing files that changed from the base of the PR and between d7cfd55 and ae3fdd2.

📒 Files selected for processing (13)
  • src/client.rs
  • src/client/adapters.rs
  • src/client/extension_lifecycle.rs
  • src/client/lifecycle.rs
  • src/features/message_edit.rs
  • src/features/mod.rs
  • src/features/rotate_key.rs
  • src/features/signal.rs
  • src/lib.rs
  • src/passkey/mod.rs
  • src/signal_flush.rs
  • src/socket/error.rs
  • src/socket/noise_socket.rs

Comment thread src/features/message_edit.rs
Comment thread src/features/message_edit.rs
Comment thread src/features/message_edit.rs
Comment thread src/features/rotate_key.rs Outdated
The only conflict was the features re-export list in src/lib.rs, where
main added MexGraphQLError, PollOptionResult, PollVoteCiphertext and Polls
while this branch added MessageEditError. Kept both.
greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 24, 2026
…uptKey

CorruptKey documents itself as material a retry would read back identically,
which is true for the staged record but not for a signature computed in this
call. A wrong width there means the signing backend broke its contract, so
InvalidState is the honest classification and leaves the retry decision right.
@greptile-apps
greptile-apps Bot dismissed their stale review July 24, 2026 18:17

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 24, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client.rs (1)

581-666: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mark the new public error types #[non_exhaustive] too.

I like that we're locking down PasskeyError and EncryptSendError with #[non_exhaustive] — that's the right call before 1.0. But ConnectStage, ConnectError, and SignalMaintenanceError are brand new, also public (re-exported at crate root and in prelude, per src/lib.rs), and just as likely to grow variants later (new connect stages, new maintenance failure classes). Leaving them exhaustive today just means we eat another breaking change the next time we add a variant. Let's be consistent and seal all four.

🛡️ Proposed fix
+#[non_exhaustive]
 pub enum ConnectStage {
     ...
 }

+#[non_exhaustive]
 pub enum ConnectError {
     ...
 }

+#[non_exhaustive]
 pub enum SignalMaintenanceError {
     ...
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client.rs` around lines 581 - 666, Mark the public ConnectStage enum with
#[non_exhaustive] to prevent future stage additions from breaking callers, and
verify ConnectError and SignalMaintenanceError retain their existing
#[non_exhaustive] annotations alongside the other public error types.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/client.rs`:
- Around line 581-666: Mark the public ConnectStage enum with #[non_exhaustive]
to prevent future stage additions from breaking callers, and verify ConnectError
and SignalMaintenanceError retain their existing #[non_exhaustive] annotations
alongside the other public error types.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 77c34d31-0fa1-4935-9dda-4297f2fb74c0

📥 Commits

Reviewing files that changed from the base of the PR and between ae3fdd2 and d069e5c.

📒 Files selected for processing (4)
  • src/client.rs
  • src/client/lifecycle.rs
  • src/features/rotate_key.rs
  • src/lib.rs

MissingTargetSender is also reached when from_me is Some(false), so the doc
saying it "was not set" described a narrower condition than the code checks.

The remoteJid branch of resolve_target_sender had no failure test even though
its sibling participant branch did, so a regression there would have been
silent.

The seven backend calls in the rotation flow each repeated the same
Storage-plus-context wrapper; a helper keeps the next one from copying it
again without changing what lands in the source chain.
@greptile-apps
greptile-apps Bot dismissed their stale review July 24, 2026 18:28

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/message_edit.rs (1)

338-339: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Handle the dispatch-result calls before formatting.

original_sender_for_dispatch(...).to_string() will not compile because the method now returns Result<Jid, MessageEditError> and Result is Display-compatible in the current setup. Wire the remaining .to_string() calls through .expect(...) or .unwrap() like the other test sites.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/message_edit.rs` around lines 338 - 339, Update the
dispatch-result handling in the method matching on self.kind to unwrap or expect
the Result<Jid, MessageEditError> returned by original_sender_for_dispatch
before calling to_string. Apply the same handling to all remaining
dispatch-result .to_string() call sites, consistent with the existing test
sites.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/features/message_edit.rs`:
- Around line 338-339: Update the dispatch-result handling in the method
matching on self.kind to unwrap or expect the Result<Jid, MessageEditError>
returned by original_sender_for_dispatch before calling to_string. Apply the
same handling to all remaining dispatch-result .to_string() call sites,
consistent with the existing test sites.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 19812b04-b114-4033-815f-df77645aecdc

📥 Commits

Reviewing files that changed from the base of the PR and between d069e5c and 0b76c33.

📒 Files selected for processing (2)
  • src/features/message_edit.rs
  • src/features/rotate_key.rs

Copy link
Copy Markdown
Collaborator Author

@coderabbitai the outside-diff "won't compile" finding on message_edit.rs:338-339 is a false positive — two different types have a method by that name. EncryptedEdit::original_sender_for_dispatch (line 205) returns Jid infallibly: it resolves from the envelope frame and never touches the target key, so it has nothing to fail on. Only SecretEncrypted::original_sender_for_dispatch (line 333) returns Result, and its call site at line 943 already unwraps. The tests at 828/834 are on EncryptedEdit, so the bare .to_string() is correct there. Clippy passed on this commit, which builds the test targets too.


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/features/message_edit.rs --items all --type impl,method,function --match 'EncryptedEdit|SecretEncrypted|original_sender_for_dispatch|original_sender_jid'
printf '\n--- declarations (Lines 180-355) ---\n'
sed -n '180,355p' src/features/message_edit.rs
printf '\n--- tests (Lines 800-850) ---\n'
sed -n '800,850p' src/features/message_edit.rs
printf '\n--- dispatch call site (Lines 920-960) ---\n'
sed -n '920,960p' src/features/message_edit.rs

Length of output: 845


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- declarations (Lines 180-355) ---'
sed -n '180,355p' src/features/message_edit.rs
printf '%s\n' '--- tests (Lines 800-850) ---'
sed -n '800,850p' src/features/message_edit.rs
printf '%s\n' '--- dispatch call site (Lines 920-960) ---'
sed -n '920,960p' src/features/message_edit.rs

Length of output: 12300


@jlucaso1 You’re right. The methods are distinct and Rust resolves them by receiver type:

  • EncryptedEdit::original_sender_for_dispatch(...) -> Jid, so the test calls at Lines 828 and 834 correctly use .to_string() directly.
  • SecretEncrypted::original_sender_for_dispatch(...) -> Result<Jid, MessageEditError>, and its relevant caller unwraps the result before .to_string().

The compilation finding incorrectly conflated those APIs. Withdrawing it.


✏️ Learnings added
Learnt from: jlucaso1
URL: https://github.com/oxidezap/whatsapp-rust/pull/1090

Timestamp: 2026-07-24T18:33:51.569Z
Learning: In `src/features/message_edit.rs`, `EncryptedEdit::original_sender_for_dispatch` returns `Jid` infallibly because it derives the author from the dispatch envelope frame, while `SecretEncrypted::original_sender_for_dispatch` returns `Result<Jid, MessageEditError>` because non-`MessageEdit` secret-encrypted kinds resolve the target key and can fail.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants