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    /// A signature verification response holds a different number of
105    /// verdicts than its request holds signatures. Retryable: no verdict is
106    /// taken from it, and the signatures may still be valid.
107    #[error("signature verification returned {got} verdicts for {expected} signatures")]
108    VerdictCount { expected: usize, got: usize },
109    /// The payload cannot be parsed. Not retryable.
110    #[error(transparent)]
111    InvalidEnvelope(#[from] xmtp_mls_validation::ValidationError),
112    /// A returned backend envelope cannot be decoded. Not retryable.
113    #[error(transparent)]
114    Envelope(#[from] xmtp_api_backend::envelope::EnvelopeError),
115    /// Proto conversion error.
116    ///
117    /// Protobuf conversion failed. Not retryable.
118    #[error(transparent)]
119    ProtoConversion(#[from] xmtp_proto::ConversionError),
120    /// The backend refused a handle claim that the validators accept. Not retryable.
121    #[error(transparent)]
122    #[error_code(inherit)]
123    HandleRejected(#[from] HandleRejection),
124}
125
126/// Why the backend refused a handle claim that the validators accept. The
127/// backend reports it in the structured `PublishError` detail of the publish.
128#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error, ErrorCode)]
129pub enum HandleRejection {
130    /// Another inbox holds the handle. Not retryable.
131    #[error("the handle is taken")]
132    Taken,
133    /// The handle's name part is reserved or shorter than the deployment's
134    /// minimum name length. Not retryable.
135    #[error("the handle is not allowed")]
136    NotAllowed,
137    /// The inbox claimed a handle too recently to claim another. Not retryable.
138    #[error("the inbox claimed a handle too recently")]
139    TooSoon,
140}
141
142impl RetryableError for ApiError {
143    fn is_retryable(&self) -> bool {
144        match self {
145            Self::Auth(e) => retryable!(e),
146            Self::Preflight(e) => retryable!(e),
147            Self::Api(e) => retryable!(e),
148            Self::VerdictCount { .. } => true,
149            _ => false,
150        }
151    }
152
153    /// The delay a rate-limit rejection asks for, read from the typed status.
154    fn rate_limit_delay(&self) -> Option<xmtp_common::time::Duration> {
155        match self {
156            Self::Api(e) => xmtp_api_grpc::error::rate_limit_delay(e),
157            Self::Preflight(e) => e.rate_limit_delay(),
158            _ => None,
159        }
160    }
161}
162
163#[derive(Debug)]
164pub struct ApiClientWrapper<ApiClient> {
165    pub api_client: preflight::GuardedApi<ApiClient>,
166    pub(crate) retry_strategy: Arc<Retry<ExponentialBackoff>>,
167    pub(crate) inbox_id: Option<String>,
168    /// What the deployment published about the shapes it accepts.
169    /// The compiled defaults until a client resolves a snapshot and installs
170    /// it, which is what keeps a bare wrapper — a test double, the static
171    /// configuration fetch — chunking exactly as it did before this existed.
172    pub(crate) configuration: Arc<xmtp_configuration::ServerConfiguration>,
173}
174
175impl<A> Clone for ApiClientWrapper<A> {
176    fn clone(&self) -> Self {
177        Self {
178            api_client: self.api_client.clone(),
179            retry_strategy: self.retry_strategy.clone(),
180            inbox_id: self.inbox_id.clone(),
181            configuration: self.configuration.clone(),
182        }
183    }
184}
185
186impl<ApiClient> ApiClientWrapper<ApiClient> {
187    pub fn new(api_client: ApiClient, retry_strategy: Retry<ExponentialBackoff>) -> Self {
188        Self {
189            api_client: preflight::GuardedApi::new(api_client),
190            retry_strategy: retry_strategy.into(),
191            inbox_id: None,
192            configuration: Arc::default(),
193        }
194    }
195
196    /// Chunk and pre-validate every later request against this snapshot.
197    /// Called once, by `build`, before the client runs.
198    pub fn set_configuration(
199        &mut self,
200        configuration: Arc<xmtp_configuration::ServerConfiguration>,
201    ) {
202        self.configuration = configuration;
203    }
204
205    /// What this wrapper chunks and pre-validates against.
206    pub fn configuration(&self) -> &xmtp_configuration::ServerConfiguration {
207        &self.configuration
208    }
209
210    /// The request shapes the deployment accepts.
211    pub fn limits(&self) -> &xmtp_configuration::LimitsConfiguration {
212        &self.configuration.limits
213    }
214
215    /// Attach an InboxId to this API Client Wrapper.
216    /// Attaches an inbox_id context to tracing logs, useful for debugging
217    pub fn attach_inbox_id(&mut self, inbox_id: Option<String>) {
218        self.inbox_id = inbox_id;
219    }
220}
221
222xmtp_common::if_native! {
223    #[cfg(test)]
224    #[ctor::ctor(unsafe)]
225    fn _setup() {
226        xmtp_common::logger()
227    }
228}
229
230#[cfg(test)]
231mod tests;