diff --git a/advanced/websocket-handling.mdx b/advanced/websocket-handling.mdx index 1f3a28f5..b20da6d9 100644 --- a/advanced/websocket-handling.mdx +++ b/advanced/websocket-handling.mdx @@ -372,7 +372,7 @@ pub fn decrypt_frame(&self, ciphertext: &[u8]) -> Result> { 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) } ``` @@ -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 { @@ -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 diff --git a/api/client.mdx b/api/client.mdx index cbcc5e41..f2883d5b 100644 --- a/api/client.mdx +++ b/api/client.mdx @@ -232,7 +232,7 @@ Waits for full connection and authentication to complete, including offline sync pub async fn pair_with_code( self: &Arc, options: PairCodeOptions, -) -> Result +) -> Result ``` 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**. @@ -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 -**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 diff --git a/api/store.mdx b/api/store.mdx index c418c198..8bea0311 100644 --- a/api/store.mdx +++ b/api/store.mdx @@ -515,9 +515,9 @@ pub struct RedisStore { impl RedisStore { pub async fn new(redis_url: &str) -> Result { 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, @@ -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(()) } @@ -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) } @@ -636,20 +636,28 @@ The `raw_id` field stores the ADV (Account Device Verification) key index list ` ## Error Handling -All storage operations return `Result` from `wacore::store::error`: +All storage operations return `Result` 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), + Validation(String), + Connection(#[source] Box), + Database(#[source] Box), + RetriesExhausted { op: String }, + Migration(#[source] Box), + InvalidConfig(String), + DeviceNotFound(i32), } pub type Result = std::result::Result; ``` + +`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. + + ## See Also - [Transport Trait](/api/transport) - Network transport abstraction diff --git a/concepts/architecture.mdx b/concepts/architecture.mdx index 48f777f3..c8ee4054 100644 --- a/concepts/architecture.mdx +++ b/concepts/architecture.mdx @@ -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 @@ -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 diff --git a/concepts/authentication.mdx b/concepts/authentication.mdx index a2a26a57..e6433aeb 100644 --- a/concepts/authentication.mdx +++ b/concepts/authentication.mdx @@ -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 diff --git a/guides/custom-backends.mdx b/guides/custom-backends.mdx index d923d107..e123170d 100644 --- a/guides/custom-backends.mdx +++ b/guides/custom-backends.mdx @@ -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", @@ -495,7 +495,7 @@ impl DeviceStore for MyCustomStore { Some(r) => { let data: Vec = 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), @@ -1141,7 +1141,7 @@ async fn load_session(&self, address: &str) -> Result>> { 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))), } } ```