Skip to main content

xmtp_api/
lib.rs

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