Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 103 additions & 1 deletion api/errors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Error hierarchy

```
Expand Down Expand Up @@ -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 `<iq type="error">` stanza the receive path decoded, kept as-is rather than reduced to the four fields [`ServerRejection`](#serverrejection) exposes above. It wraps the same `Arc<OwnedNodeRef>` the success path already hands back, so attaching it to the error costs one refcount bump, not a copy.

```rust
pub struct RejectionStanza(Arc<OwnedNodeRef>);

impl RejectionStanza {
/// The preserved node behind its refcount, for a caller that wants to keep or share it.
pub fn as_arc(&self) -> &Arc<OwnedNodeRef>;
/// Takes the preserved node out, consuming the wrapper.
pub fn into_arc(self) -> Arc<OwnedNodeRef>;
}

impl From<Arc<OwnedNodeRef>> 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 `<iq>`/`<error>` attributes, `<error>` children such as XMPP application-condition elements, and the raw bytes, which are the only faithful material for logging or replaying a rejection. `Deref<Target = OwnedNodeRef>` (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`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Split the RejectionStanza explanation into focused sentences

This paragraph combines parser behavior, preserved data categories, logging and replay implications, dereference mechanics, and accessor examples into two dense sentences. Split these independent ideas into concise sentences so the new reference follows the project's one-idea-per-sentence requirement.

AGENTS.md reference: AGENTS.md:L24-L25

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Same style question CodeRabbit raised on this PR (line 14) — skipping for the same reason: the surrounding unchanged content on this page is consistently dense, third-person, rationale-heavy prose (e.g. the http_status() bullet above, or the ServerRejection line), and I matched that established voice deliberately rather than switching tone only for the new sections. CodeRabbit agreed and withdrew its equivalent finding. Happy to revisit if a maintainer wants the whole page's voice changed.


Generated by Claude Code


`Debug` is overridden to print only the tag (`<iq>`), 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.
Expand Down Expand Up @@ -557,7 +586,13 @@ pub enum IqError {
#[error("received disconnect node during IQ wait: {0:?}")]
Disconnected(Box<Node>),
#[error("received a server error response: code={code}, text='{text}'")]
ServerError { code: u16, text: String, error_type: Option<String>, backoff: Option<u32> },
ServerError {
code: u16,
text: String,
error_type: Option<String>,
backoff: Option<u32>,
response: RejectionStanza, // added in PR #1257
},
#[error("received unexpected IQ response type: {got:?}")]
UnexpectedResponseType { got: Option<String> },
#[error("internal channel closed unexpectedly")]
Expand All @@ -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. 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

### From `anyhow::Error`
Expand Down Expand Up @@ -701,3 +738,68 @@ 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, .. } => { /* ... */ }
// ...
}
```

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
// 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<OwnedNodeRef>, RejectionStanza converts via `.into()`
}
```

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 = wacore_binary::builder::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<OwnedNodeRef> = Arc::new(OwnedNodeRef::new(node_bytes).expect("decodes"));
```

`From<wacore::request::IqError> 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);
```