Skip to content

feat(client): let a consumer take a stanza before the built-in pipeline - #1239

Merged
jlucaso1 merged 6 commits into
mainfrom
feat/stanza-interceptors
Aug 8, 2026
Merged

feat(client): let a consumer take a stanza before the built-in pipeline#1239
jlucaso1 merged 6 commits into
mainfrom
feat/stanza-interceptors

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

The client models the stanzas it knows about and nacks the rest. That is the
right default — a <nack> tells the server this client cannot act on something,
and staying silent would leave the stanza in the offline queue indefinitely.

What is missing is a way to say otherwise. A stanza this version does not model
gets nacked whether or not the application would have known what to do with it,
and StanzaRouter::register panics on a duplicate tag, so even an existing tag
cannot be handled differently. Today, extending the client means forking it.

This adds a seam:

impl StanzaInterceptor for Vendor {
    fn intercept(&self, node: &OwnedNodeRef) -> Interception {
        if node.tag() == "vendor:thing" {
            // … act on it …
            Interception::Handled
        } else {
            Interception::Pass
        }
    }
}

let handle = client.add_stanza_interceptor(Arc::new(Vendor));

An interceptor runs before dispatch, sees every decoded stanza, and either steps
aside or claims it. A claimed stanza skips the built-in pipeline and is
acknowledged exactly as it would have been, so the server does not redeliver.

Design

Claiming skips handling, not housekeeping. Offline-sync tracking,
response-waiter resolution and stream shutdown all run before dispatch, and they
keep running whether or not a stanza is claimed. They are what keeps the
connection working; an interceptor that could switch them off would be a way to
break a client rather than to extend one. A test pins this: <ib> offline
tracking still runs on a claimed <ib>.

The ack is unconditional. A claimed stanza is acked the way it would have
been. Withholding it would leave the stanza queued for redelivery, which is a
worse failure than double-handling — and the interceptor is claiming
responsibility for the stanza, not for the transport.

Free while unused. One relaxed atomic on the read loop, checked before the
lock is touched. Same shape as the existing raw-node forwarding lease, so the
cost model is one people already know: registering is what turns the check into
a walk.

Registration order is priority order. The first interceptor to claim a
stanza wins and the rest are skipped, so an earlier registration can shadow a
later one. Rejected the alternative of letting every interceptor see every
stanza and then reconciling their answers: two consumers both claiming one
stanza is a conflict the client cannot resolve, and picking a winner by
registration order at least makes it predictable.

The handle is RAII and weak. Dropping it unregisters. It holds a weak client
reference, so a forgotten handle cannot pin a client alive, and dropping one
after its client is gone is a no-op rather than a panic.

Not panic-caught. Same contract as EventHandler: this is called directly
by the read loop, an unwind there takes the connection with it, and a directly
registered interceptor is trusted. The plugin host already catches panics from
plugins, so the untrusted path stays covered.

What this is not

Not a filter for outbound stanzas, and not a decryption hook — a claimed
<message> has already been decrypted or not by the time an interceptor sees
it, exactly as the built-in handler would have found it.

Verification

cargo nextest run -p whatsapp-rust --lib client::tests
cargo clippy --workspace --all-targets -- -D warnings
cargo test -p whatsapp-rust --doc
RUSTDOCFLAGS="-D warnings" cargo doc -p whatsapp-rust --no-deps

Eleven tests, covering what the design claims rather than that the code runs:

  • an interceptor sees every decoded stanza, and stops seeing them when its
    handle drops;
  • passing leaves the built-in handler to dispatch its event; claiming stops it,
    asserted through the event bus rather than by inspecting internals;
  • offline-sync tracking runs on a claimed stanza;
  • the first claimer wins, and a passing interceptor does not shadow the next;
  • a claimed unknown tag does not reach the nack path;
  • a closure is an interceptor;
  • a handle outliving its client drops cleanly.

@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 Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added support for registering custom stanza interceptors.
    • Interceptors can observe, pass through, or handle stanzas before normal processing.
    • Added closure-based interceptors and handles that automatically unregister when released.
    • Interceptors run in registration order, with public APIs available for integration.
    • Added a way to check whether stanza tags have registered handlers.
    • Added interceptor counts to memory reports.
  • Bug Fixes

    • Preserved acknowledgement and bookkeeping behavior for intercepted and unknown stanzas.
    • Protected connection-critical stanzas from interception to maintain reliable client operation.

Walkthrough

The client now supports public stanza interceptors. Interceptors can observe or claim decoded stanzas, unregister through drop-based handles, and preserve acknowledgement and housekeeping behavior.

Changes

Stanza interception

Layer / File(s) Summary
Interceptor contract and client state
src/client/interceptor.rs, src/client.rs, src/client/lifecycle.rs, src/lib.rs
Adds public interception types, registration storage, client initialization, memory reporting, documentation, unit tests, and crate-level exports.
Registration and lifecycle management
src/client/accessors.rs
Adds ordered registration and removal, copy-on-write snapshots, active-count checks, unique IDs, and lock-poison recovery.
Pre-dispatch execution and validation
src/client/node_io.rs, src/handlers/router.rs, src/client/tests.rs
Runs interceptors before built-in routing for non-critical stanzas. Handled stanzas retain acknowledgement behavior. Tests cover ordering, pass-through, claiming, unregistering, closures, unknown stanzas, protected stanzas, and handle lifetime.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant StanzaInterceptor
  participant StanzaRouter
  participant Acknowledgement
  Client->>StanzaInterceptor: intercept decoded non-critical stanza
  StanzaInterceptor-->>Client: Pass or Handled
  alt Handled
    Client->>Acknowledgement: preserve deferred or generated acknowledgement
  else Pass
    Client->>StanzaRouter: continue built-in routing
  end
Loading

Suggested labels: api-design

Suggested reviewers: greptile-apps

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: allowing consumers to intercept stanzas before built-in processing.
Description check ✅ Passed The description directly explains stanza interception, claiming behavior, acknowledgements, lifecycle, design decisions, and verification.
Docstring Coverage ✅ Passed Docstring coverage is 95.45% 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 feat/stanza-interceptors

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.

@greptile-apps

greptile-apps Bot commented Aug 7, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds an ordered, RAII-managed stanza interception API that lets consumers claim decoded stanzas before built-in dispatch while preserving connection housekeeping and transport acknowledgments.

  • Adds public interceptor traits, decisions, registration handles, and memory reporting.
  • Runs interceptors before stanza dispatch, with explicit exclusions for connection-critical stanzas and server pings.
  • Acknowledges claimed unknown stanzas instead of sending the normal unrecognized-stanza nack.
  • Adds coverage for registration order, unregistration, acknowledgments, housekeeping, critical controls, and ping handling.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/client/node_io.rs Integrates interception into inbound dispatch while retaining housekeeping, protecting control stanzas and pings, and acknowledging claimed unknown tags.
src/client/interceptor.rs Defines the public interception contract, decision type, closure support, and weak RAII registration handle.
src/client/accessors.rs Implements ordered copy-on-write interceptor registration, removal, snapshots, and reporting.
src/client/tests.rs Adds end-to-end coverage for interception priority, lifecycle, dispatch suppression, acknowledgment behavior, and protected stanza classes.
src/handlers/router.rs Adds tag-membership lookup used to distinguish modeled stanzas from unknown tags when selecting acknowledgment behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Decoded stanza] --> B[Offline sync and waiter housekeeping]
    B --> C{Connection-critical or server ping?}
    C -- Yes --> F[Built-in dispatch]
    C -- No --> D{Interceptor claims stanza?}
    D -- No --> F
    D -- Yes --> E[Send applicable transport acknowledgment]
    E --> G[Skip built-in pipeline]
    F --> H[Built-in handler or unknown-stanza nack]
Loading

Reviews (7): Last reviewed commit: "fix(client): never offer a server ping t..." | Re-trigger Greptile

Comment thread src/client/node_io.rs Outdated
Comment thread src/client/node_io.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: 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/client/accessors.rs`:
- Around line 82-85: Update the interceptor count atomic operations in the
registration and access paths, including stanza_interceptor_count.store calls
and the fast-path load in stanza_interceptors_guard(), to use Ordering::Relaxed.
Keep synchronization for registry access within stanza_interceptors_guard() and
preserve the existing lock decision logic.

In `@src/client/node_io.rs`:
- Around line 508-520: Update the interceptor-claimed branch in the stanza
processing flow around should_ack, deferred_ack_node, and intercept_stanza so
claimed unknown stanzas with valid ACK identity fields receive a generic
transport ACK even when should_ack() is false. Preserve the existing deferred
ACK behavior for known stanzas, retain the unknown-stanza nack path when no
interceptor claims the stanza, and add a regression test that verifies the
outbound ACK.
- Around line 570-587: Update the decoded-stanza handling in
process_decrypted_node and process_node so intercept_stanza runs for ACKs,
matched IQ responses, and xmlstreamend before returning. Preserve the existing
response-waiter resolution and stream-shutdown housekeeping first, then invoke
interception on every decoded stanza; add regression coverage for all three
paths.
🪄 Autofix

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: 72a8edaa-d054-4750-85dd-f50d61480d1a

📥 Commits

Reviewing files that changed from the base of the PR and between f9dd253 and c8470db.

📒 Files selected for processing (7)
  • src/client.rs
  • src/client/accessors.rs
  • src/client/interceptor.rs
  • src/client/lifecycle.rs
  • src/client/node_io.rs
  • src/client/tests.rs
  • src/lib.rs

Comment thread src/client/accessors.rs Outdated
Comment thread src/client/node_io.rs
Comment thread src/client/node_io.rs

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 7 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/client/node_io.rs Outdated
Comment thread src/client/interceptor.rs Outdated
Comment thread src/client/tests.rs
Comment thread src/client/tests.rs
Comment thread src/client/node_io.rs Outdated
Comment thread src/client/accessors.rs Outdated
@jlucaso1

jlucaso1 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Three findings, all valid, all fixed in 2ab2155.

Claimed unknown stanzas went unanswered (greptile P1). Confirmed and the worst of the three. should_ack covers only receipt | notification | call | message | status, so a claimed vendor:thing fell through with neither the ack nor the nack it would otherwise have received. That is precisely the failure nack_unrecognized_stanza exists to prevent — its own comment records an unhandled <status> recycling the stream.

A claim now turns that nack into an ack: both settle the stanza, and which one is right depends on whether anyone handled it. Same identity requirement as the nack path — without id and from there is nothing to address.

My test asserted the old behaviour as if it were the feature. It was testing that the stanza was not nacked, which was true and beside the point.

Interception bypassed connection state (greptile P1). Also valid, and the fix is a list rather than a warning: success, failure, stream:error and ack are no longer offered to an interceptor at all. They settle authentication, shutdown and the waiters a send blocks on, and claiming one would leave a client authenticated-but-unaware, never reconnecting, or waiting forever on a completed send.

zapo protects the same auth tags from its own stanza filters for the same reason, which is a good sign the line is in the right place.

<iq> is deliberately still offered — most <iq> traffic is exactly what a consumer would want to extend, and blocking it would remove most of the point. Claiming a ping or a pairing step leaves the server without its reply, so the docs now say to match narrowly.

Relaxed ordering (coderabbit). Valid, and I had already caught it while auditing the cost of this PR: the docs promised one relaxed load while the code used Acquire/Release. The lock behind the count does the synchronising, so the count only decides whether to take it. Now Relaxed throughout, and the doc comment says why.


Two things not raised that the same audit turned up:

The registry now uses the copy-on-write snapshot the event bus already uses, so reading it is a refcount bump rather than a Vec allocation per stanza. The previous version allocated on every stanza once any interceptor was registered.

The module docs claimed an interceptor "sees every decoded stanza". It does not: an already-correlated IQ response, <xmlstreamend>, and now the connection-critical tags return before dispatch. Corrected to say it sees what would have reached dispatch, and to point at Event::RawNode for observing everything. Two tools, two jobs — one watches, one takes over.

Four tests added for the new behaviour, and the existing 135 in client::tests still pass.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 7, 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.

Actionable comments posted: 2

🤖 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/client/interceptor.rs`:
- Around line 49-52: Update the module documentation around the claimed-stanza
acknowledgement guarantee to state that acknowledgement occurs only when the
stanza has both an id and a from address. Keep the existing ack/nack behavior
description, but qualify the claim so it matches the dispatch path’s
precondition.

In `@src/client/tests.rs`:
- Around line 5143-5145: Update the test around client.process_node and the
existing seen assertion to await the test transport output, then verify it
contains exactly one ACK for V-1 and no NACK. Keep the interceptor invocation
assertion while adding response assertions that fail when neither stanza is
emitted.
🪄 Autofix

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: 958c3920-0a91-4879-befd-900ba3c7e9af

📥 Commits

Reviewing files that changed from the base of the PR and between c8470db and 2ab2155.

📒 Files selected for processing (6)
  • src/client.rs
  • src/client/accessors.rs
  • src/client/interceptor.rs
  • src/client/lifecycle.rs
  • src/client/node_io.rs
  • src/client/tests.rs

Comment thread src/client/interceptor.rs Outdated
Comment thread src/client/tests.rs

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 6 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/client/node_io.rs Outdated
Comment thread src/client/interceptor.rs Outdated
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.03 MiB 10.03 MiB +1.84 KiB (+0.02%) 🔺
bin .text 8.04 MiB 8.04 MiB +1.69 KiB (+0.02%) 🔺
bin allocated (text+data+bss) 10.03 MiB 10.03 MiB +3.98 KiB (+0.04%) 🔺
llvm-lines wacore 520,610 520,610 0
llvm-lines wacore copies 17,006 17,006 0
llvm-lines whatsapp-rust lib 738,465 740,416 +1,951 (+0.26%) 🔺
llvm-lines whatsapp-rust lib copies 23,190 23,250 +60 (+0.26%) 🔺
deps crates (Cargo.lock) 462 462 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.82 MiB 1.83 MiB +1.65 KiB (+0.09%) 🔺
.text wacore 695.80 KiB 696.10 KiB +312 B (+0.04%) 🔺
.text wacore_binary 88.30 KiB 88.30 KiB 0
.text wacore_libsignal 178.88 KiB 178.88 KiB 0
.text wacore_appstate 22.35 KiB 22.35 KiB 0
.text wacore_noise 20.94 KiB 20.94 KiB 0
.text waproto 1.74 MiB 1.74 MiB 0
.text whatsapp_rust_sqlite_storage 540.30 KiB 540.30 KiB 0
.text whatsapp_rust_tokio_transport 40.49 KiB 40.49 KiB 0
.text whatsapp_rust_ureq_http_client 12.53 KiB 12.53 KiB 0
.text std 993.79 KiB 993.81 KiB +24 B (+0.00%) 🔺
.text other deps 1.90 MiB 1.90 MiB -312 B (-0.02%) 🔽
Top movers (cargo-bloat attribution)
Crate main PR Δ
whatsapp_rust 1.82 MiB 1.83 MiB +1.65 KiB (+0.09%)

Baseline: 74deed425 (latest main run) · Head: 63f572510 · Graphs

@greptile-apps
greptile-apps Bot dismissed their stale review August 7, 2026 22:06

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

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 7, 2026

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/client/interceptor.rs Outdated
@greptile-apps
greptile-apps Bot dismissed their stale review August 7, 2026 22:47

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

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 2 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Adds a public interpolation API and changes stanza dispatch and ack/nack semantics, including design choices about priority ordering and non-claimable stanzas; a human should sign off on this protocol-facing contract.

Re-trigger cubic

jlucaso1 and others added 4 commits August 7, 2026 20:23
The client models the stanzas it knows and nacks the rest, which is the
right default — a nack tells the server this client cannot act on
something, and silence would leave it queued forever.

But there is no way to say otherwise. A stanza this version does not
model is nacked whether or not the application would have known what to
do with it, and StanzaRouter::register panics on a duplicate tag, so
even an existing tag cannot be handled differently. Extending the client
means forking it.

An interceptor is that room. It runs before dispatch, sees every decoded
stanza, and either steps aside or claims it. A claimed stanza skips the
built-in pipeline and is acked exactly as it would have been, so the
server does not redeliver.

Claiming skips handling, not housekeeping. Offline-sync tracking,
response-waiter resolution and stream shutdown run before dispatch and
keep running either way — they are what keeps the connection working,
and an interceptor able to switch them off would be a way to break a
client rather than extend one.

Free while unused: one relaxed atomic on the read loop, the same shape
as the raw-node forwarding lease.
Three findings from review, all real.

An unmodelled stanza that an interceptor claimed was left unanswered.
should_ack covers only the tags the client models, so a claimed
vendor tag fell through with neither ack nor the nack it would have
received — the exact shape of the bug the unknown-stanza nack exists to
prevent, where the stanza stays queued and the stream keeps recycling.
A claim now turns that nack into an ack, since someone did handle it.

success, failure, stream:error and ack are no longer offered at all.
They settle authentication, shutdown and the waiters a send blocks on;
claiming one would leave a client authenticated-but-unaware, or never
reconnecting, or waiting forever on a send that already completed. zapo
protects the same auth tags from its own stanza filters, for the reason.

The count is read Relaxed rather than Acquire. The lock behind it does
the synchronising, so the fast path did not need the stronger ordering
the docs already claimed it did not have.

Also switches the registry to the copy-on-write snapshot the event bus
uses, so reading it costs a refcount bump instead of allocating a Vec
per stanza, and corrects the module docs: an interceptor sees what would
have reached dispatch, not everything decoded. Event::RawNode is the
tool for everything.
The claimed-stanza ack covered any stanza with `id` and `from` that
`should_ack` had not already matched. That is wider than the case it was
added for: a claimed direct `<message>` is answered with a delivery
`<receipt>` and a claimed `<iq>` with an `<iq type="result">`, and neither
is an `<ack class="…">`. The server was being sent something it did not
ask for.

Narrow it to tags the router does not model — the stanzas that would have
been nacked, which is the gap a claim is meant to close. A tag the client
models but answers some other way now gets nothing: whoever claimed it
took on the reply, and inventing an answer is worse than silence.

Two documentation claims were also wider than the code. Acknowledgement
needs `id` and `from`, and the trait sees stanzas headed for dispatch
rather than every decoded stanza — which the module documentation already
said, one paragraph away.

The claimed-unknown test now decodes the outbound frame instead of only
proving the interceptor ran, so it fails if the ack stops being sent.
…nswered

`should_ack` covers `receipt`, `notification` and `call`, so a claimed
one still draws the transport ack it always did. The acknowledgement
section used a delivery receipt as its example of a tag the client
answers some other way — which would have a reader believe they owed a
reply the client is still sending, or avoid claiming receipts at all.

The group the paragraph is about is direct `<message>` and `<iq>`. The
tags that keep their ack are now named as such.
@jlucaso1
jlucaso1 force-pushed the feat/stanza-interceptors branch from f2bff93 to fa1f511 Compare August 7, 2026 23:24
Comment thread src/client/node_io.rs

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

🤖 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/client/interceptor.rs`:
- Around line 13-15: Qualify the acknowledgement guarantees in
src/client/interceptor.rs at lines 13-15 and 120-122: document that claiming a
stanza suppresses the built-in handler, and for direct message or built-in IQ
stanzas the claimant must provide any required protocol-specific reply because
no generic transport ACK is sent. Update both the module-level text and
Interception::Handled documentation consistently.
🪄 Autofix

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: a08db514-6c7d-464f-b391-84d4e1134398

📥 Commits

Reviewing files that changed from the base of the PR and between e864445 and fa1f511.

📒 Files selected for processing (4)
  • src/client.rs
  • src/client/accessors.rs
  • src/client/interceptor.rs
  • src/client/node_io.rs

Comment thread src/client/interceptor.rs Outdated
`report_coverage` requires every `Client` field that can grow to reach
`memory_report()`, and this one can: a handle that outlives its interest
leaves an interceptor registered, and each one costs a walk on every
stanza. Exempting it would hide exactly the leak worth seeing.

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 2 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Adds a new public interceptor API and changes the inbound stanza dispatch/ack path, letting consumers claim stanzas before built-in handlers. This is a behavioral/public-contract change that needs human review of its semantics and edge cases.

Re-trigger cubic

A claimed ping is a pong never sent, and the server closes the connection
over it. That is the same harm `success`, `failure`, `stream:error` and
`ack` are protected from — an interceptor exists to extend a client, not
to leave it disconnected — so a server-initiated `<iq>` ping joins them.

The gate is `handle_iq`'s own ping test, extracted so the two cannot drift
apart: what the client answers is exactly what it refuses to hand over. A
ping *response* carries no obligation and stays offered, as does every
other `<iq>` — that is the traffic an interceptor is for.

Two documentation claims also still promised an acknowledgement for every
claimed stanza. The acknowledgement section had already been corrected;
the module introduction and `Interception::Handled` had not.

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 3 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Auto-approved: The diff shows the interceptor seam is optional and zero-cost when unused, passes only stanzas that would dispatch and blocks connection-critical ones, and stays consistent with existing patterns.

Re-trigger cubic

@jlucaso1
jlucaso1 merged commit 1363cb2 into main Aug 8, 2026
33 checks passed
@jlucaso1
jlucaso1 deleted the feat/stanza-interceptors branch August 8, 2026 01:07
jlucaso1 added a commit to oxidezap/wa-wire that referenced this pull request Aug 8, 2026
Tap rides Event::RawNode and only watches. Takeover rides the
pre-dispatch interceptor added upstream in oxidezap/whatsapp-rust#1239:
a claimed stanza skips the engine's handler and is acked all the same,
so the server does not redeliver.

The two carry separate capability sets, because neither is a superset of
the other. Tap sees the authentication exchange and cannot suppress
anything. Takeover suppresses but cannot see that exchange — the engine
refuses to offer success, failure, stream:error and ack to an
interceptor, since a consumer that took one would leave the client
authenticated-but-unaware or waiting forever on a completed send. One
declaration for both would be false in one direction.

A policy decides per stanza rather than all-or-nothing: an engine
reduced to a transport still has to be told what to do with the rest.
TakeTags is the common case, TakeEverything the full one.

Forwarding happens before the decision, so a claimed stanza is never one
the consumer did not receive — the engine will not handle it either, so
it would simply vanish. For the same reason a poisoned sink passes
rather than claims.
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.

1 participant