From 130d945a8616cfb235d5d2efd844bc683701aaf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Sat, 8 Aug 2026 09:17:26 -0300 Subject: [PATCH 1/3] docs(errors): document RejectionStanza and the ServerError.response field Reflects oxidezap/whatsapp-rust#1257: IqError::ServerError gains a response: RejectionStanza field carrying the rejection stanza verbatim, and From is replaced by IqError::from_response. --- api/errors.mdx | 84 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/api/errors.mdx b/api/errors.mdx index b0bd5736..8f605cd3 100644 --- a/api/errors.mdx +++ b/api/errors.mdx @@ -11,6 +11,8 @@ As of PR #1100, every wrapping variant that used to be `#[error(transparent)]` i As of PR #1195, `ErrorChainExt` also answers `http_status()` — the HTTP status code behind a refused download, upload, sticker-pack fetch, or app-version fetch, recovered from a [`HttpStatusError`](#httpstatuserror) node the same way `server_rejection()` recovers an IQ rejection. This is the one place the "lower-level APIs … may still surface `anyhow::Error` directly" caveat above gets a typed escape hatch: `download`/`upload` still return `anyhow::Error`, but the status inside that error is now recoverable by type instead of by parsing `Display` text. See [Error chain recovery](#error-chain-recovery) below. +As of PR #1257, `IqError::ServerError` also carries `response: RejectionStanza` — the `type="error"` stanza itself, handed over whole the same way a `type="result"` response already was, alongside the four fields this crate parses off it (`code`, `text`, `error_type`, `backoff`). See [`RejectionStanza`](#rejectionstanza) below. + ## Error hierarchy ``` @@ -147,6 +149,33 @@ pub struct ServerRejection<'a> { Borrowed from whichever error in the chain carried it, so recovering one costs no allocation. +### RejectionStanza + +Added in PR #1257. `IqError::ServerError`'s `response` field: the `` stanza the receive path decoded, kept as-is rather than reduced to the four fields [`ServerRejection`](#serverrejection) exposes above. It wraps the same `Arc` the success path already hands back, so attaching it to the error costs one refcount bump, not a copy. + +```rust +pub struct RejectionStanza(Arc); + +impl RejectionStanza { + /// The preserved node behind its refcount, for a caller that wants to keep or share it. + pub fn as_arc(&self) -> &Arc; + /// Takes the preserved node out, consuming the wrapper. + pub fn into_arc(self) -> Arc; +} + +impl From> for RejectionStanza { /* ... */ } + +impl Deref for RejectionStanza { + type Target = OwnedNodeRef; +} +``` + +`ServerRejection`'s four fields cover what WA Web's own `parseIqResponse` reads off an error; `RejectionStanza` is the escape hatch for everything that parser (and this crate's) leaves unread — further ``/`` attributes, `` children such as XMPP application-condition elements, and the raw bytes, which are the only faithful material for logging or replaying a rejection. `Deref` (see [`OwnedNodeRef`](/advanced/binary-protocol#ownednoderef-yoke-zero-copy)) keeps every node accessor reachable directly on the wrapper — `response.tag()`, `response.attrs()`, `response.get_optional_child(...)`, or `response.get()` for the underlying `NodeRef`. + +`Debug` is overridden to print only the tag (``), not the stanza's contents. This matters because background IQ failures on the connect path (the post-connect active IQ, props, blocklist, privacy settings) are logged with `{e:?}` at warn level, and an error stanza's attributes or children can carry a JID — a straight derive would have written that into production logs where before only the four summarized fields went. Read the node explicitly (`response.get()`, `response.attrs()`, …) when you need its contents. + +Re-exported from the crate root as `whatsapp_rust::RejectionStanza`, and from `whatsapp_rust::prelude`. + ### HttpStatusError Added in PR #1195, in `whatsapp_rust::http`. Carries the status of an HTTP exchange the client refused — the source node `http_status()` looks for. @@ -557,7 +586,13 @@ pub enum IqError { #[error("received disconnect node during IQ wait: {0:?}")] Disconnected(Box), #[error("received a server error response: code={code}, text='{text}'")] - ServerError { code: u16, text: String, error_type: Option, backoff: Option }, + ServerError { + code: u16, + text: String, + error_type: Option, + backoff: Option, + response: RejectionStanza, // added in PR #1257 + }, #[error("received unexpected IQ response type: {got:?}")] UnexpectedResponseType { got: Option }, #[error("internal channel closed unexpectedly")] @@ -577,6 +612,8 @@ pub enum IqError { Added in PR #1100: `wacore::request::IqError` gained public `is_timeout()` (`true` only for `Timeout`) and `is_transport_unavailable()` (`true` for `NotConnected`, `Disconnected`, and `InternalChannelClosed`) methods, each an exhaustive match so a future variant has to be classified rather than silently defaulting to `false`. `whatsapp_rust::request::IqError` (the crate-level type shown above, with the extra `Socket`/`EncryptSend`/`ClientState`/`EncodeError`/`ParseError` variants) makes the same judgement internally but does not expose it publicly — go through [`ErrorChainExt`](#error-chain-recovery) instead, which handles both types. +Added in PR #1257: `ServerError` carries `response: RejectionStanza` — see [`RejectionStanza`](#rejectionstanza) above and the [migration note](#from-pr-1257-servererror-carries-the-rejection-stanza) below. Matching with `..` is unaffected by this field; only hand-constructing the variant (mainly test fixtures) needs updating. + ## Migration guide ### From `anyhow::Error` @@ -701,3 +738,48 @@ match client.download(downloadable).await { ``` Messages are unchanged either way — `http_status()` is purely additive, recovering a fact that was already in the text but previously reachable only by parsing it. + +### From PR #1257: `ServerError` carries the rejection stanza + +`IqError::ServerError` gained a `response: RejectionStanza` field carrying the `type="error"` stanza verbatim, alongside the four fields it already parsed off it. Matching with `..` is unaffected — this is the common case, and nearly every match in the codebase already used it: + +```rust +// Still works unchanged +match err { + IqError::ServerError { code, text, .. } => { /* ... */ } + // ... +} +``` + +Constructing the variant by hand — mainly test fixtures — now needs the stanza it was rejected with: + +```rust +// Before +IqError::ServerError { + code, + text, + error_type, + backoff, +} + +// After — pass the response the rejection came with +IqError::ServerError { + code, + text, + error_type, + backoff, + response: response.into(), // response: Arc, RejectionStanza converts via `.into()` +} +``` + +A fixture with no real wire response can build one directly: decode a hand-built `` through `OwnedNodeRef::new`, wrap it in `Arc`, and convert. + +`From for whatsapp_rust::request::IqError` is also removed — a bare `From` conversion has no response available to attach, which is exactly the material this change stops discarding. Replace it with `IqError::from_response`: + +```rust +// Before +let err: IqError = wacore_err.into(); + +// After — pass the response the classification failure was read from +let err = IqError::from_response(wacore_err, &response); +``` From 30840189b02abcf9041a9477017c230c37a3a1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Sat, 8 Aug 2026 09:27:06 -0300 Subject: [PATCH 2/3] =?UTF-8?q?docs(errors):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20E0027=20exhaustive-match=20break,=20fixture=20unpac?= =?UTF-8?q?k=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Note that a ServerError match binding all four former fields without `..` now fails E0027 once `response` is added, not just hand-constructed variants. - Show the required unpack() step when building a fixture from marshal() output for OwnedNodeRef::new, which expects the format byte already stripped. --- api/errors.mdx | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/api/errors.mdx b/api/errors.mdx index 8f605cd3..0bbb7fae 100644 --- a/api/errors.mdx +++ b/api/errors.mdx @@ -612,7 +612,7 @@ pub enum IqError { Added in PR #1100: `wacore::request::IqError` gained public `is_timeout()` (`true` only for `Timeout`) and `is_transport_unavailable()` (`true` for `NotConnected`, `Disconnected`, and `InternalChannelClosed`) methods, each an exhaustive match so a future variant has to be classified rather than silently defaulting to `false`. `whatsapp_rust::request::IqError` (the crate-level type shown above, with the extra `Socket`/`EncryptSend`/`ClientState`/`EncodeError`/`ParseError` variants) makes the same judgement internally but does not expose it publicly — go through [`ErrorChainExt`](#error-chain-recovery) instead, which handles both types. -Added in PR #1257: `ServerError` carries `response: RejectionStanza` — see [`RejectionStanza`](#rejectionstanza) above and the [migration note](#from-pr-1257-servererror-carries-the-rejection-stanza) below. Matching with `..` is unaffected by this field; only hand-constructing the variant (mainly test fixtures) needs updating. +Added in PR #1257: `ServerError` carries `response: RejectionStanza` — see [`RejectionStanza`](#rejectionstanza) above and the [migration note](#from-pr-1257-servererror-carries-the-rejection-stanza) below. Matching with `..` is unaffected by this field. A match that already names all four former fields without `..` needs `..` added (or `response` bound too) — Rust rejects a struct pattern missing a field with `E0027`. Constructing the variant by hand (mainly test fixtures) needs updating the same way. ## Migration guide @@ -751,6 +751,16 @@ match err { } ``` +If you matched all four former fields by name without `..`, the pattern now fails to compile with `E0027` ("pattern does not mention field `response`") — add `..`, or bind `response` too: + +```rust +// Before — breaks with E0027 once `response` is added +IqError::ServerError { code, text, error_type, backoff } => { /* ... */ } + +// After +IqError::ServerError { code, text, error_type, backoff, .. } => { /* ... */ } +``` + Constructing the variant by hand — mainly test fixtures — now needs the stanza it was rejected with: ```rust @@ -772,7 +782,17 @@ IqError::ServerError { } ``` -A fixture with no real wire response can build one directly: decode a hand-built `` through `OwnedNodeRef::new`, wrap it in `Arc`, and convert. +A fixture with no real wire response can build one directly, but `OwnedNodeRef::new` expects node bytes with the format byte already stripped, not the raw output of `marshal` — the same [`unpack`](/advanced/binary-protocol#the-format-byte) step the receive path runs ahead of every `OwnedNodeRef::new` call: + +```rust +let node = NodeBuilder::new("iq") + .attr("type", "error") + .children([/* ... */]) + .build(); +let packed = wacore_binary::marshal::marshal(&node).expect("marshals"); +let node_bytes = wacore_binary::util::unpack(&packed).expect("unpacks").into_owned(); +let response: Arc = Arc::new(OwnedNodeRef::new(node_bytes).expect("decodes")); +``` `From for whatsapp_rust::request::IqError` is also removed — a bare `From` conversion has no response available to attach, which is exactly the material this change stops discarding. Replace it with `IqError::from_response`: From bfa6d8e820b783fae11a11bf8daa58afeae37b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= <55464917+jlucaso1@users.noreply.github.com> Date: Sat, 8 Aug 2026 09:33:08 -0300 Subject: [PATCH 3/3] docs(errors): fully qualify NodeBuilder in fixture snippet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches the fully-qualified style already used for marshal/unpack in the same snippet, per cubic's review — the bare NodeBuilder reference wasn't importable as written. --- api/errors.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/errors.mdx b/api/errors.mdx index 0bbb7fae..b2d3af1a 100644 --- a/api/errors.mdx +++ b/api/errors.mdx @@ -785,7 +785,7 @@ IqError::ServerError { A fixture with no real wire response can build one directly, but `OwnedNodeRef::new` expects node bytes with the format byte already stripped, not the raw output of `marshal` — the same [`unpack`](/advanced/binary-protocol#the-format-byte) step the receive path runs ahead of every `OwnedNodeRef::new` call: ```rust -let node = NodeBuilder::new("iq") +let node = wacore_binary::builder::NodeBuilder::new("iq") .attr("type", "error") .children([/* ... */]) .build();