Skip to main content

xmtp_api/
lib.rs

1#![warn(clippy::unwrap_used)]
2
3pub mod attachments;
4pub mod configuration;
5pub mod identity;
6pub mod mls;
7mod notification;
8pub mod preflight;
9pub mod scw_verifier;
10#[cfg(any(test, feature = "test-utils"))]
11pub mod test_utils;
12
13use std::sync::Arc;
14
15use xmtp_common::{ErrorCode, ExponentialBackoff, Retry, RetryableError, retryable};
16pub use xmtp_proto::api_client::XmtpApi;
17
18pub use identity::*;
19pub use mls::*;
20pub mod chunk;
21pub use chunk::PublishUnit;
22
23pub type Result<T> = std::result::Result<T, ApiError>;
24
25pub mod strategies {
26    use super::*;
27    pub fn exponential_cooldown() -> Retry<ExponentialBackoff> {
28        xmtp_common::Retry::builder().build()
29    }
30}
31
32/// Preserve auth codes before transport errors lose their concrete type.
33///
34/// The error may arrive boxed, and `Box<E>` implements `RetryableError`, so a
35/// downcast of the outer value alone would miss the code. Check the source
36/// chain too: `ApiClientError::Auth` is `#[error(transparent)]` over the
37/// `AuthError`.
38// implements: AUTH-026
39pub fn dyn_err(e: impl RetryableError + 'static) -> ApiError {
40    fn find(error: &(dyn std::any::Any + 'static)) -> Option<xmtp_proto::api::AuthError> {
41        if let Some(ApiError::Auth(auth)) = error.downcast_ref() {
42            return Some(*auth);
43        }
44        if let Some(boxed) = error.downcast_ref::<Box<ApiError>>() {
45            return find(&**boxed);
46        }
47        if let Some(xmtp_proto::api::ApiClientError::Auth(auth)) = error.downcast_ref() {
48            return Some(*auth);
49        }
50        if let Some(auth) = error.downcast_ref::<xmtp_proto::api::AuthError>() {
51            return Some(*auth);
52        }
53        // A boxed error downcasts as the box, never as its contents. Unwrap the
54        // shapes that reach this function; `source()` cannot help, because
55        // `#[error(transparent)]` forwards Display without forwarding `source`.
56        if let Some(boxed) = error.downcast_ref::<Box<xmtp_proto::api::ApiClientError>>() {
57            return find(&**boxed);
58        }
59        if let Some(boxed) = error.downcast_ref::<Box<xmtp_proto::api::AuthError>>() {
60            return Some(**boxed);
61        }
62        None
63    }
64    if let Some(error) = preflight::failure(&e) {
65        return ApiError::Preflight(error.clone());
66    }
67    if let Some(auth) = find(&e) {
68        return ApiError::Auth(auth);
69    }
70    ApiError::Api(xmtp_proto::api::NetworkError::new(e))
71}
72
73#[derive(Debug, thiserror::Error, ErrorCode)]
74pub enum ApiError {
75    /// Configuration admission failed before dispatch. May be retryable on a new operation.
76    #[error("request preflight failed: {0}")]
77    Preflight(#[source] preflight::PreflightError),
78    #[error(transparent)]
79    #[error_code(inherit)]
80    Auth(#[from] xmtp_proto::api::AuthError),
81    /// API client error.
82    ///
83    /// API operation error (network, deserialization, or other). May be retryable.
84    #[error("api client error {0}")]
85    Api(#[source] xmtp_proto::api::NetworkError),
86    /// The backend rejected a stale identity update. Not retryable here.
87    #[error("identity history changed")]
88    IdentityUpdateConflict,
89    /// One envelope exceeds the configured byte limit. Not retryable.
90    #[error("envelope exceeds the byte limit")]
91    EnvelopeTooLarge,
92    /// One atomic publish unit exceeds a request limit. Not retryable.
93    #[error("atomic publish unit exceeds a request limit")]
94    UnitTooLarge,
95    /// A single-topic response still exceeds a backend limit. Not retryable.
96    #[error("one envelope response exceeds a backend limit")]
97    ResponseTooLarge,
98    /// The request has invalid input. Not retryable.
99    #[error("invalid backend request: {0}")]
100    InvalidRequest(&'static str),
101    /// A response does not match the request. Not retryable.
102    #[error("invalid backend response: {0}")]
103    InvalidResponse(&'static str),
104    /// The payload cannot be parsed. Not retryable.
105    #[error(transparent)]
106    InvalidEnvelope(#[from] xmtp_mls_validation::ValidationError),
107    /// A returned backend envelope cannot be decoded. Not retryable.
108    #[error(transparent)]
109    Envelope(#[from] xmtp_api_backend::envelope::EnvelopeError),
110    /// Proto conversion error.
111    ///
112    /// Protobuf conversion failed. Not retryable.
113    #[error(transparent)]
114    ProtoConversion(#[from] xmtp_proto::ConversionError),
115    /// The backend refused a handle claim that the validators accept. Not retryable.
116    #[error(transparent)]
117    #[error_code(inherit)]
118    HandleRejected(#[from] HandleRejection),
119}
120
121/// Why the backend refused a handle claim that the validators accept. The
122/// backend reports it in the structured `PublishError` detail of the publish.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error, ErrorCode)]
124pub enum HandleRejection {
125    /// Another inbox holds the handle. Not retryable.
126    #[error("the handle is taken")]
127    Taken,
128    /// The handle's name part is reserved or shorter than the deployment's
129    /// minimum name length. Not retryable.
130    #[error("the handle is not allowed")]
131    NotAllowed,
132    /// The inbox claimed a handle too recently to claim another. Not retryable.
133    #[error("the inbox claimed a handle too recently")]
134    TooSoon,
135}
136
137impl RetryableError for ApiError {
138    fn is_retryable(&self) -> bool {
139        match self {
140            Self::Auth(e) => retryable!(e),
141            Self::Preflight(e) => retryable!(e),
142            Self::Api(e) => retryable!(e),
143            _ => false,
144        }
145    }
146}
147
148#[derive(Debug)]
149pub struct ApiClientWrapper<ApiClient> {
150    pub api_client: preflight::GuardedApi<ApiClient>,
151    pub(crate) retry_strategy: Arc<Retry<ExponentialBackoff>>,
152    pub(crate) inbox_id: Option<String>,
153    /// What the deployment published about the shapes it accepts.
154    /// The compiled defaults until a client resolves a snapshot and installs
155    /// it, which is what keeps a bare wrapper — a test double, the static
156    /// configuration fetch — chunking exactly as it did before this existed.
157    pub(crate) configuration: Arc<xmtp_configuration::ServerConfiguration>,
158}
159
160impl<A> Clone for ApiClientWrapper<A> {
161    fn clone(&self) -> Self {
162        Self {
163            api_client: self.api_client.clone(),
164            retry_strategy: self.retry_strategy.clone(),
165            inbox_id: self.inbox_id.clone(),
166            configuration: self.configuration.clone(),
167        }
168    }
169}
170
171impl<ApiClient> ApiClientWrapper<ApiClient> {
172    pub fn new(api_client: ApiClient, retry_strategy: Retry<ExponentialBackoff>) -> Self {
173        Self {
174            api_client: preflight::GuardedApi::new(api_client),
175            retry_strategy: retry_strategy.into(),
176            inbox_id: None,
177            configuration: Arc::default(),
178        }
179    }
180
181    /// Chunk and pre-validate every later request against this snapshot.
182    /// Called once, by `build`, before the client runs.
183    pub fn set_configuration(
184        &mut self,
185        configuration: Arc<xmtp_configuration::ServerConfiguration>,
186    ) {
187        self.configuration = configuration;
188    }
189
190    /// What this wrapper chunks and pre-validates against.
191    pub fn configuration(&self) -> &xmtp_configuration::ServerConfiguration {
192        &self.configuration
193    }
194
195    /// The request shapes the deployment accepts.
196    pub fn limits(&self) -> &xmtp_configuration::LimitsConfiguration {
197        &self.configuration.limits
198    }
199
200    /// Attach an InboxId to this API Client Wrapper.
201    /// Attaches an inbox_id context to tracing logs, useful for debugging
202    pub fn attach_inbox_id(&mut self, inbox_id: Option<String>) {
203        self.inbox_id = inbox_id;
204    }
205}
206
207xmtp_common::if_native! {
208    #[cfg(test)]
209    #[ctor::ctor(unsafe)]
210    fn _setup() {
211        xmtp_common::logger()
212    }
213}
214
215#[cfg(test)]
216mod tests;