Skip to main content

xmtp_api_backend/middleware/
readonly_client.rs

1//! We define a very simple strategy for disabling writes on certain clients.
2
3xmtp_common::if_test! {
4    mod test;
5}
6
7use derive_builder::Builder;
8use prost::bytes::Bytes;
9use xmtp_proto::api::{ApiClientError, Client};
10use xmtp_proto::api::{BytesStream, IsConnectedCheck};
11
12const PUBLISH_PATH: &str = "/xmtp.backend.v1.PublishService/Publish";
13
14/// A client that will error on requests that write to the network.
15#[derive(Debug, Builder, Default, Clone)]
16#[builder(public)]
17pub struct ReadonlyClient<Client> {
18    #[builder(public)]
19    pub(crate) inner: Client,
20}
21
22impl<C: Clone> ReadonlyClient<C> {
23    pub fn builder() -> ReadonlyClientBuilder<C> {
24        ReadonlyClientBuilder::default()
25    }
26}
27
28#[xmtp_common::async_trait]
29impl<C> Client for ReadonlyClient<C>
30where
31    C: Client,
32{
33    fn host(&self) -> &str {
34        self.inner.host()
35    }
36
37    fn has_credential_source(&self) -> bool {
38        self.inner.has_credential_source()
39    }
40
41    async fn request(
42        &self,
43        request: http::request::Builder,
44        path: http::uri::PathAndQuery,
45        body: Bytes,
46    ) -> Result<http::Response<Bytes>, ApiClientError> {
47        let p = path.path();
48        if p == PUBLISH_PATH {
49            return Err(ApiClientError::WritesDisabled);
50        }
51
52        self.inner.request(request, path, body).await
53    }
54
55    async fn stream(
56        &self,
57        request: http::request::Builder,
58        path: http::uri::PathAndQuery,
59        body: Bytes,
60    ) -> Result<http::Response<BytesStream>, ApiClientError> {
61        let p = path.path();
62        if p == PUBLISH_PATH {
63            return Err(ApiClientError::WritesDisabled);
64        }
65
66        self.inner.stream(request, path, body).await
67    }
68
69    async fn bidi_stream(
70        &self,
71        request: http::request::Builder,
72        path: http::uri::PathAndQuery,
73        body: xmtp_common::BoxDynStream<'static, Bytes>,
74    ) -> Result<http::Response<BytesStream>, ApiClientError> {
75        let p = path.path();
76        if p == PUBLISH_PATH {
77            return Err(ApiClientError::WritesDisabled);
78        }
79
80        self.inner.bidi_stream(request, path, body).await
81    }
82}
83
84#[xmtp_common::async_trait]
85impl<C> IsConnectedCheck for ReadonlyClient<C>
86where
87    C: IsConnectedCheck,
88{
89    async fn is_connected(&self) -> bool {
90        self.inner.is_connected().await
91    }
92}
93
94xmtp_common::if_test! {
95    use derive_builder::UninitializedFieldError;
96    use xmtp_proto::prelude::ApiBuilder;
97    #[allow(clippy::unwrap_used)]
98    impl<C> ReadonlyClientBuilder<C>
99    where
100        C: ApiBuilder,
101    {
102        pub(crate) fn build_builder(
103            self,
104        ) -> Result<ReadonlyClient<C::Output>, UninitializedFieldError> {
105            Ok(ReadonlyClient {
106                inner: <C as ApiBuilder>::build(
107                    self.inner
108                        .ok_or(UninitializedFieldError::new("read"))
109                        .unwrap(),
110                )
111                .unwrap(),
112            })
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use crate::backend::{GetInboxIds, Publish};
120
121    use super::*;
122    use rstest::*;
123
124    use xmtp_proto::api::{Query, mock::MockNetworkClient};
125    type MockClient = ReadonlyClient<MockNetworkClient>;
126
127    #[fixture]
128    fn ro() -> MockClient {
129        ReadonlyClient {
130            inner: MockNetworkClient::default(),
131        }
132    }
133
134    #[rstest]
135    #[xmtp_common::test(unwrap_try = true)]
136    async fn test_forwards_to_inner(mut ro: MockClient) {
137        ro.inner
138            .expect_request()
139            .times(1)
140            .returning(|_, _, _| Ok(http::Response::new(vec![].into())));
141        let mut e = GetInboxIds(Default::default());
142        e.query(&ro).await?;
143    }
144
145    #[rstest]
146    #[xmtp_common::test(unwrap_try = true)]
147    async fn test_errors_on_write(ro: MockClient) {
148        let mut e = Publish(Default::default());
149        let result = e.query(&ro).await;
150        assert!(matches!(result, Err(ApiClientError::WritesDisabled)));
151    }
152}