Skip to content

fix(offline): transport-ack stanzas with only unrecognized enc types - #648

Merged
jlucaso1 merged 3 commits into
mainfrom
fix/offline-unknown-enc-ack
May 28, 2026
Merged

jlucaso1 merged 3 commits into
mainfrom
fix/offline-unknown-enc-ack

Conversation

@jlucaso1

Copy link
Copy Markdown
Collaborator

Summary

  • After PR fix(offline): drain offline queue by acking duplicate and undecryptable messages #647 the dominant remaining cause of reconnects in prod is stanzas whose enc nodes are all unknown to the client (e.g. msmsg from the Meta AI bot). Classify silently dropped them, so the server replayed them every reconnect until <stream:error> closed the stream.
  • This PR adds a fallback in classify_incoming_message: when both session_payloads and group_payloads end up empty AND at least one enc was unknown AND no custom handler claimed any enc, spawn a transport <ack> to drop the stanza from the offline queue. status@broadcast is left alone (the should_ack gate already covers it).
  • Decryption of msmsg itself is intentionally out of scope; a follow-up PR will implement it. This change just stops the offline queue from looping on stanzas we cannot decrypt yet.

Test plan

  • cargo fmt --all
  • cargo clippy --all --tests (clean)
  • cargo test -p whatsapp-rust -p wacore -p waproto (all green)
  • New tests: unknown-only enc gets a transport ack; status@broadcast skips it; recognized + unknown still classifies normally; custom-handler-only does not double-ack.

A stanza whose enc nodes are all unknown to the client (e.g. msmsg from the
Meta AI bot) used to silently fall through classify_incoming_message without
any ack, so the server replayed it from the offline queue every reconnect
until <stream:error> closed the stream.

Track had_unknown_enc and had_custom_handler in the enc loop; when both
session_payloads and group_payloads are empty, an unknown enc was seen, and
no custom handler claimed any enc, spawn a transport ack and return None.
status@broadcast is skipped here because should_ack already covers it.
Custom-handler-only flows are left untouched because the handler is
responsible for its own ack.

Decryption of msmsg itself is a follow-up; this PR just stops the offline
queue from looping on stanzas we cannot decrypt yet.
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4734f4b9-5f82-45b9-b2fd-ec82ec5112ef

📥 Commits

Reviewing files that changed from the base of the PR and between 1d749d2 and 5184f5b.

📒 Files selected for processing (2)
  • src/message.rs
  • tests/e2e/src/lib.rs

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Unknown-only encrypted messages now emit a transport-level acknowledgement and are removed from offline queues (status broadcasts excluded).
    • Custom handlers for unknown encryption types suppress the fallback ack and are invoked once.
    • Recognized-but-empty or mixed known+unknown payloads no longer trigger the fallback ack.
  • Tests

    • Added async tests covering unknown-only ack behavior, suppression by custom handlers, status-broadcast exception, and mixed/empty payload cases.

Walkthrough

Detects incoming stanzas whose <enc> nodes are all unknown/unhandled, records per-message flags, emits a transport <ack class="message"> via a new client helper (excluding status@broadcast), and short-circuits by returning None; async tests and an e2e tweak validate behavior.

Changes

Unknown-only encryption payload transport ack

Layer / File(s) Summary
State tracking flags for unknown-only detection
src/message.rs
Add had_unknown_enc and had_custom_handler booleans to classification state.
ENC iteration and flag updates
src/message.rs
Mark missing type as unknown, set had_custom_handler when a custom handler claims a payload, mark unknown enc_type and payload-extraction failures as unknown, and skip processing for those nodes.
Unknown-only fallback ack emission & client ack helper
src/message.rs, src/client.rs
If no session/group payloads were produced, at least one unknown <enc> was seen, and no custom handler claimed anything, call spawn_node_transport_ack (skip status@broadcast) and return None; add Client::spawn_node_transport_ack to encode and enqueue transport ACK bytes via outbound_flush.
Async tests for unknown-only ack behavior
src/message.rs (tests)
New async tests cover unknown-only fallback ack, recipient echo, status@broadcast exclusion, known-empty enc not triggering fallback, mixed recognized+unknown cases, and custom handler suppression/invocation.
E2E client delivery receipt forcing
tests/e2e/src/lib.rs
Enable forced active delivery receipts in TestClient::connect_inner when push_name is pre-seeded to avoid inactive delivery receipts in the test mutation path.

Sequence Diagram(s)

sequenceDiagram
  participant Classifier
  participant Client
  participant OutboundFlush
  participant Server
  Classifier->>Classifier: detect had_unknown_enc && !had_custom_handler && no session/group payloads
  Classifier->>Client: spawn_node_transport_ack(node_ref)
  Client->>Client: encode_ack_bytes(node_ref, own_pn)
  Client->>OutboundFlush: outbound_flush.spawn(encoded_ack_bytes)
  OutboundFlush->>Server: send transport <ack class="message">
  Server-->>Classifier: stanza removed/dropped from offline queue
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a transport-ack fallback for offline stanzas with only unrecognized enc types, addressing the reconnect loop issue from PR #647.
Description check ✅ Passed The description is well-related to the changeset, clearly explaining the problem (offline queue looping on unknown enc types), the solution (transport ack fallback), scope boundaries, and test coverage.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/offline-unknown-enc-ack

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 and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46a5a83cde

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/message.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: 0

Caution

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

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

572-622: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't treat known-but-empty <enc> nodes like unknown-only traffic.

EncPayload::from_owned_node() returns None for both unknown enc types and known encs with missing content. Right now both set had_unknown_enc, so a stanza like <enc type="msg"/> takes the new transport-ack fallback and gets dropped as if it were msmsg. Split that bookkeeping before the fallback.

Suggested fix
             let payload = match EncPayload::from_owned_node(node, enc_node) {
                 Some(p) => p,
                 None => {
-                    log::warn!("Enc node has no content or unknown type: {enc_type}");
-                    had_unknown_enc = true;
+                    if EncType::from_wire(enc_type.as_ref()).is_none() {
+                        log::warn!("Enc node has unknown type: {enc_type}");
+                        had_unknown_enc = true;
+                    } else {
+                        log::warn!("Enc node has no content: {enc_type}");
+                    }
                     continue;
                 }
             };
🤖 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/message.rs` around lines 572 - 622, The code treats both unknown enc
types and known-but-empty <enc> nodes the same because
EncPayload::from_owned_node(node, enc_node) returns None in both cases; update
the match branch that currently sets had_unknown_enc to distinguish unknown enc
types from known-but-empty content: use the enc_type value (or call
EncType::from_wire_str / equivalent) to detect if the enc type is recognized and
only set had_unknown_enc when the type is unrecognized, leaving known-but-empty
enc nodes unflagged so session_payloads/group_payloads logic and the
transport-ack fallback (which checks session_payloads, group_payloads,
had_unknown_enc, had_custom_handler and calls self.spawn_message_ack(&info))
behave correctly.
🤖 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/message.rs`:
- Around line 572-622: The code treats both unknown enc types and
known-but-empty <enc> nodes the same because EncPayload::from_owned_node(node,
enc_node) returns None in both cases; update the match branch that currently
sets had_unknown_enc to distinguish unknown enc types from known-but-empty
content: use the enc_type value (or call EncType::from_wire_str / equivalent) to
detect if the enc type is recognized and only set had_unknown_enc when the type
is unrecognized, leaving known-but-empty enc nodes unflagged so
session_payloads/group_payloads logic and the transport-ack fallback (which
checks session_payloads, group_payloads, had_unknown_enc, had_custom_handler and
calls self.spawn_message_ack(&info)) behave correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 801479d8-9dcf-47a9-b320-6c9394e7e2e6

📥 Commits

Reviewing files that changed from the base of the PR and between ebb9faf and 46a5a83.

📒 Files selected for processing (1)
  • src/message.rs

…epair e2e

Codex P2: the unknown-enc fallback was reconstructing the ack from
MessageInfo, but parse_message_info only retains `recipient` on the self-sent
branch, so LID-routed / hosted-companion / peer stanzas lost the attribute
and the server replied <stream:error><ack/>. Add
`Client::spawn_node_transport_ack` which encodes from the original NodeRef so
`recipient` is echoed verbatim, and use it in the classify fallback.

Outside-diff comment: `EncPayload::from_owned_node` returns None for both
unknown types and known-but-empty content. Resolve `EncType::from_wire` up
front and only set `had_unknown_enc` when the type itself is unrecognized;
known-but-empty (malformed) falls back through the regular flow.

Repair the e2e regression introduced by PR #647: `with_push_name` pre-seeds
the name, so the setting_pushName mutation arrives with `old == new` and
skips the auto `presence().set_available()` that would flip
send_active_receipts to 1. As a result delivery receipts went out as
type="inactive" and `test_bidirectional_offline_receipt` /
`test_deferred_delivery_receipt_on_reconnect` failed in CI. Force active
receipts in `TestClient::connect_inner` (mirrors whatsmeow's
`SetForceActiveDeliveryReceipts`).

Tests:
- `unknown_only_enc_ack_preserves_recipient` for the LID-routed ack shape.
- `known_enc_type_with_empty_content_skips_fallback_ack` for the new gate.

@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: 3

🤖 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/message.rs`:
- Around line 577-581: When EncPayload::from_owned_node(node, enc_node) returns
None for a known enc_type, don't just log and continue — mark the message as a
terminal malformed stanza so it will be handled by the malformed path. Replace
the current continue branch in the match by setting the flag used to indicate
non-processable encs (e.g., set had_unknown_enc / had_malformed_enc to true or
otherwise mark the message as malformed), stop further enc-node processing so
classification yields a malformed ClassifiedMessage, and let
process_classified_message emit the appropriate ack/nack; reference
EncPayload::from_owned_node, enc_node, enc_type, had_unknown_enc, and
ClassifiedMessage to locate the change.

In `@tests/e2e/src/lib.rs`:
- Around line 140-142: Replace the three-line explanatory comment above the call
that uses with_push_name with a single concise sentence stating: "with_push_name
pre-seeds the name so the setting_pushName mutation has old==new (skipping auto
set_available), so force active to keep delivery receipts from being
type='inactive'." Reference the same context (with_push_name and
setting_pushName) and remove the play‑by‑play lines.
- Around line 140-143: The current workaround unconditionally calls
client.set_force_active_delivery_receipts(true), forcing active receipts for all
e2e clients; change this to only set forced active receipts when the client was
created with a pre-seeded push name by checking the push_name Optional (i.e., if
push_name.is_some()) before calling set_force_active_delivery_receipts; locate
the client creation code path used by connect_without_push_name/connect_*
helpers and wrap the set_force_active_delivery_receipts(true) call in a
conditional that verifies push_name.is_some() so clients created with None keep
normal receipt behavior.
🪄 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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f48ddd06-8c94-4e38-af69-ee072357fecb

📥 Commits

Reviewing files that changed from the base of the PR and between 46a5a83 and 1d749d2.

📒 Files selected for processing (3)
  • src/client.rs
  • src/message.rs
  • tests/e2e/src/lib.rs

Comment thread src/message.rs
Comment thread tests/e2e/src/lib.rs Outdated
Comment thread tests/e2e/src/lib.rs Outdated
- Known-type-with-empty content also produces no usable payload, so include
  it in had_unknown_enc. process_classified_message has no branch for both
  payload vectors empty, so without flagging the stanza would loop in the
  offline queue too.
- Invert the known-but-empty test to assert the ack is now emitted.
- e2e: trim the workaround comment and gate set_force_active_delivery_receipts
  on push_name.is_some() so connect_without_push_name keeps the natural
  setting_pushName-triggered set_available path.
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant