Skip to content
Merged
Show file tree
Hide file tree
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
10 changes: 6 additions & 4 deletions advanced/websocket-handling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ pub fn decrypt_frame(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
let counter = self.read_counter.fetch_add(1, Ordering::SeqCst);
self.read_key
.decrypt_with_counter(counter, ciphertext)
.map_err(|e| SocketError::Crypto(e.to_string()))
.map_err(SocketError::Cipher)
}
```

Expand Down Expand Up @@ -721,9 +721,9 @@ This is critical for the keepalive loop and stanza acknowledgment, both of which
```rust
pub enum SocketError {
SocketClosed,
NoiseHandshake(String),
Io(String),
Crypto(String),
Io(#[from] std::io::Error),
Cipher(#[from] NoiseError),
Marshal(#[source] BinaryError),
}

pub enum EncryptSendError {
Expand All @@ -735,6 +735,8 @@ pub enum EncryptSendError {
}
```

Each variant preserves the underlying typed error as a `source()`. `Cipher` wraps a `NoiseError` (from `wacore::handshake`), which itself carries a typed `CryptoProviderError` source. Walking the chain with `std::error::Error::source()` lets callers downcast to the original AES-GCM, libsignal, or binary-protocol error without parsing strings.

All variants return buffers for reuse:

```rust
Expand Down
22 changes: 14 additions & 8 deletions api/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ Waits for full connection and authentication to complete, including offline sync
pub async fn pair_with_code(
self: &Arc<Self>,
options: PairCodeOptions,
) -> Result<String, PairCodeError>
) -> Result<String, PairError>
```

Initiates pair code authentication as an alternative to QR code pairing. The returned 8-character code should be displayed to the user, who enters it on their phone under **WhatsApp > Linked Devices > Link a Device > Link with phone number instead**.
Expand All @@ -251,16 +251,22 @@ This can run concurrently with QR code pairing — whichever completes first win
The 8-character pairing code to display to the user
</ResponseField>

**Errors (`PairCodeError`):**
**Errors (`PairError`):**

`PairError::PairCode(PairCodeError)` covers validation and crypto failures; `PairError::RequestFailed(IqError)` covers the IQ transport.

| Variant | Cause |
|---------|-------|
| `PhoneNumberRequired` | Empty phone number |
| `PhoneNumberTooShort` | Fewer than 7 digits |
| `PhoneNumberNotInternational` | Starts with `0` (not international format) |
| `InvalidCustomCode` | Custom code is not 8 valid Crockford Base32 characters |
| `MissingPairingRef` | Server response missing pairing ref |
| `RequestFailed` | Server request failed |
| `PairCode(PhoneNumberRequired)` | Empty phone number |
| `PairCode(PhoneNumberTooShort)` | Fewer than 7 digits |
| `PairCode(PhoneNumberNotInternational)` | Starts with `0` (not international format) |
| `PairCode(InvalidCustomCode)` | Custom code is not 8 valid Crockford Base32 characters |
| `PairCode(InvalidPrimaryEphemeralKey)` / `InvalidPrimaryIdentityKey` | Peer key parsing failed (typed `CurveError` source) |
| `PairCode(EphemeralKeyAgreement)` / `IdentityKeyAgreement` | Diffie–Hellman failed (typed `CurveError` source) |
| `PairCode(AdvSecretKeyDerivation)` / `BundleKeyDerivation` | HKDF expand failed |
| `PairCode(BundleAead)` | AES-GCM encryption of the key bundle failed (typed `CryptoProviderError` source) |
| `PairCode(MissingPairingRef)` | Server response missing pairing ref |
| `RequestFailed` | Server IQ request failed (typed `IqError` source) |

**Example:**
```rust
Expand Down
28 changes: 18 additions & 10 deletions api/store.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -515,9 +515,9 @@ pub struct RedisStore {
impl RedisStore {
pub async fn new(redis_url: &str) -> Result<Self> {
let client = redis::Client::open(redis_url)
.map_err(|e| StoreError::Connection(e.to_string()))?;
.map_err(|e| StoreError::Connection(Box::new(e)))?;
let conn = client.get_connection_manager().await
.map_err(|e| StoreError::Connection(e.to_string()))?;
.map_err(|e| StoreError::Connection(Box::new(e)))?;

Ok(Self {
client: conn,
Expand All @@ -536,7 +536,7 @@ impl SignalStore for RedisStore {
.arg(&key[..])
.query_async(&mut conn)
.await
.map_err(|e| StoreError::Database(e.to_string()))?;
.map_err(|e| StoreError::Database(Box::new(e)))?;
Ok(())
}

Expand All @@ -547,7 +547,7 @@ impl SignalStore for RedisStore {
.arg(key_name)
.query_async(&mut conn)
.await
.map_err(|e| StoreError::Database(e.to_string()))?;
.map_err(|e| StoreError::Database(Box::new(e)))?;
Ok(result)
}

Expand Down Expand Up @@ -636,20 +636,28 @@ The `raw_id` field stores the ADV (Account Device Verification) key index list `

## Error Handling

All storage operations return `Result<T>` from `wacore::store::error`:
All storage operations return `Result<T>` from `wacore::store::error`. Each variant preserves the underlying typed error as its `source()` so callers can downcast to the original backend error when needed:

```rust
pub enum StoreError {
Connection(String), // Connection failures
Database(String), // Database operation errors
Migration(String), // Migration errors
Serialization(String), // Serialization/deserialization errors
NotFound, // Resource not found
Io(#[from] std::io::Error),
Serialization(#[source] Box<dyn std::error::Error + Send + Sync>),
Validation(String),
Connection(#[source] Box<dyn std::error::Error + Send + Sync>),
Database(#[source] Box<dyn std::error::Error + Send + Sync>),
RetriesExhausted { op: String },
Migration(#[source] Box<dyn std::error::Error + Send + Sync>),
InvalidConfig(String),
DeviceNotFound(i32),
}

pub type Result<T> = std::result::Result<T, StoreError>;
```

<Note>
`StoreError` exposes a helper `is_database_busy_or_locked()` that walks the source chain looking for SQLite `BUSY`/`LOCKED` markers. Retry layers use it to decide whether a database error is transient without depending on a specific backend crate.
</Note>

## See Also

- [Transport Trait](/api/transport) - Network transport abstraction
Expand Down
10 changes: 6 additions & 4 deletions concepts/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -627,10 +627,10 @@ use anyhow::Result;

#[derive(Debug, Error)]
pub enum SocketError {
#[error("connection closed")]
Closed,
#[error("encryption failed: {0}")]
Encryption(String),
#[error("socket is closed")]
SocketClosed,
#[error("noise cipher operation failed")]
Cipher(#[from] NoiseError),
}

// Use anyhow::Result for functions with multiple error types
Expand All @@ -642,6 +642,8 @@ pub async fn complex_operation() -> Result<()> {
}
```

Error variants across the workspace preserve typed sources (via `#[from]` or `#[source]`) instead of stringifying inner errors. Callers can walk `std::error::Error::source()` to downcast to the original cause.

## Related Sections

<CardGroup cols={2}>
Expand Down
46 changes: 35 additions & 11 deletions concepts/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -544,41 +544,65 @@ client.disconnect().await;

### Pair Code Errors

`pair_with_code` returns `whatsapp_rust::pair_code::PairError`, which wraps the wacore-side validation/crypto errors (`PairCodeError`) and the IQ transport layer (`IqError`):

```rust
use whatsapp_rust::pair_code::PairError;
use wacore::pair_code::PairCodeError;

match client.pair_with_code(options).await {
Ok(code) => println!("Code: {}", code),
Err(PairCodeError::PhoneNumberRequired) => {
Err(PairError::PairCode(PairCodeError::PhoneNumberRequired)) => {
eprintln!("Phone number is required");
}
Err(PairCodeError::PhoneNumberTooShort) => {
Err(PairError::PairCode(PairCodeError::PhoneNumberTooShort)) => {
eprintln!("Phone number must be at least 7 digits");
}
Err(PairCodeError::PhoneNumberNotInternational) => {
Err(PairError::PairCode(PairCodeError::PhoneNumberNotInternational)) => {
eprintln!("Phone number must not start with 0 (use international format)");
}
Err(PairCodeError::InvalidCustomCode) => {
Err(PairError::PairCode(PairCodeError::InvalidCustomCode)) => {
eprintln!("Custom code must be 8 valid Crockford Base32 characters");
}
Err(PairCodeError::MissingPairingRef) => {
Err(PairError::PairCode(PairCodeError::MissingPairingRef)) => {
eprintln!("Server did not return a pairing reference");
}
Err(PairCodeError::NotWaiting) => {
Err(PairError::PairCode(PairCodeError::NotWaiting)) => {
eprintln!("No pending pair code request");
}
Err(PairCodeError::InvalidWrappedData { expected, got }) => {
Err(PairError::PairCode(PairCodeError::InvalidWrappedData { expected, got })) => {
eprintln!("Invalid wrapped data: expected {} bytes, got {}", expected, got);
}
Err(PairCodeError::CryptoError(msg)) => {
eprintln!("Crypto error during pairing: {}", msg);
// Typed crypto failures preserve their `CurveError`/`CryptoProviderError` source
Err(PairError::PairCode(PairCodeError::InvalidPrimaryEphemeralKey(e))) => {
eprintln!("Primary device sent an invalid ephemeral key: {e}");
}
Err(PairError::PairCode(PairCodeError::InvalidPrimaryIdentityKey(e))) => {
eprintln!("Primary device sent an invalid identity key: {e}");
}
Err(PairError::PairCode(PairCodeError::EphemeralKeyAgreement(e))) => {
eprintln!("Ephemeral DH failed: {e}");
}
Err(PairError::PairCode(PairCodeError::IdentityKeyAgreement(e))) => {
eprintln!("Identity DH failed: {e}");
}
Err(PairCodeError::RequestFailed(msg)) => {
eprintln!("Pairing request failed: {}", msg);
Err(PairError::PairCode(PairCodeError::AdvSecretKeyDerivation)) => {
eprintln!("HKDF expand for adv_secret failed");
}
Err(PairError::PairCode(PairCodeError::BundleKeyDerivation)) => {
eprintln!("HKDF expand for bundle encryption key failed");
}
Err(PairError::PairCode(PairCodeError::BundleAead(e))) => {
eprintln!("AES-GCM encryption of key bundle failed: {e}");
}
Err(PairError::RequestFailed(iq)) => {
eprintln!("Pair-code IQ request failed: {iq}");
}
}
```

The previous catch-all `CryptoError(String)` and `RequestFailed(String)` variants have been split into typed variants that preserve their underlying source. Match on `std::error::Error::source()` (or downcast it) to inspect the inner `CurveError`, `CryptoProviderError`, or `IqError`.

## Session Persistence

### After Successful Pairing
Expand Down
6 changes: 3 additions & 3 deletions guides/custom-backends.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ impl DeviceStore for MyCustomStore {
async fn save(&self, device: &Device) -> Result<()> {
// Serialize device data (use your preferred format)
let serialized = bincode::serialize(device)
.map_err(|e| StoreError::Serialization(e.to_string()))?;
.map_err(|e| StoreError::Serialization(Box::new(e)))?;

self.connection.execute(
"UPDATE devices SET data = ? WHERE id = 1",
Expand All @@ -495,7 +495,7 @@ impl DeviceStore for MyCustomStore {
Some(r) => {
let data: Vec<u8> = r.get(0);
let device = bincode::deserialize(&data)
.map_err(|e| StoreError::Deserialization(e.to_string()))?;
.map_err(|e| StoreError::Serialization(Box::new(e)))?;
Ok(Some(device))
}
None => Ok(None),
Expand Down Expand Up @@ -1141,7 +1141,7 @@ async fn load_session(&self, address: &str) -> Result<Option<Vec<u8>>> {
match self.connection.query("SELECT ...", &[address]).await {
Ok(row) => Ok(Some(row.get(0))),
Err(e) if is_not_found(&e) => Ok(None),
Err(e) => Err(StoreError::Database(e.to_string())),
Err(e) => Err(StoreError::Database(Box::new(e))),
}
}
```
Expand Down