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