Skip to main content

fedimint_connectors/
http.rs

1use std::sync::Arc;
2
3use anyhow::anyhow;
4use fedimint_core::util::SafeUrl;
5use fedimint_core::{apply, async_trait_maybe_send};
6use reqwest::{Method, StatusCode};
7use serde_json::Value;
8
9use crate::error::ServerError;
10use crate::{
11    Connectivity, DynGatewayConnection, DynGuaridianConnection, IConnection, IGatewayConnection,
12    ServerResult,
13};
14
15#[derive(Clone, Debug, Default)]
16pub(crate) struct HttpConnector {
17    client: Arc<reqwest::Client>,
18}
19
20#[async_trait::async_trait]
21impl crate::Connector for HttpConnector {
22    async fn connect_guardian(
23        &self,
24        _url: &SafeUrl,
25        _api_secret: Option<&str>,
26    ) -> ServerResult<DynGuaridianConnection> {
27        Err(ServerError::InternalClientError(anyhow!(
28            "Unsupported transport mechanism"
29        )))
30    }
31
32    async fn connect_gateway(&self, url: &SafeUrl) -> anyhow::Result<DynGatewayConnection> {
33        let http_connection = HttpConnection {
34            client: self.client.clone(),
35            base_url: url.clone(),
36        };
37
38        Ok(IGatewayConnection::into_dyn(http_connection))
39    }
40
41    fn connectivity(&self, _url: &SafeUrl) -> Connectivity {
42        Connectivity::Direct
43    }
44}
45
46#[derive(Debug)]
47pub(crate) struct HttpConnection {
48    client: Arc<reqwest::Client>,
49    base_url: SafeUrl,
50}
51
52#[apply(async_trait_maybe_send!)]
53impl IConnection for HttpConnection {
54    async fn await_disconnection(&self) {
55        // `HttpConnection` is a stateless wrapper over a pooled `reqwest::Client`;
56        // it never actually disconnects. Returning immediately would make the
57        // `ConnectionPool` treat every request as a reconnection and impose its
58        // reconnect backoff (a 500ms floor) before each one, so we pend forever.
59        std::future::pending().await
60    }
61
62    fn is_connected(&self) -> bool {
63        // `reqwest::Client` handles TCP/TLS connection pooling internally, so this
64        // wrapper is always "connected" and safe to cache. Reporting `false` here
65        // forced the `ConnectionPool` to reset the entry to a reconnecting state on
66        // every request, penalizing each one with a 500ms reconnect-backoff sleep
67        // even though the underlying connection was reused the whole time.
68        true
69    }
70}
71
72#[apply(async_trait_maybe_send!)]
73impl IGatewayConnection for HttpConnection {
74    async fn request(
75        &self,
76        password: Option<String>,
77        method: Method,
78        route: &str,
79        payload: Option<Value>,
80    ) -> ServerResult<Value> {
81        let url = self.base_url.join(route).expect("Invalid base url");
82        let mut builder = self.client.request(method, url.clone().to_unsafe());
83        if let Some(password) = password.clone() {
84            builder = builder.bearer_auth(password);
85        }
86        if let Some(payload) = payload {
87            builder = builder.json(&payload);
88        }
89
90        let response = builder
91            .send()
92            .await
93            .map_err(|e| ServerError::ServerError(e.into()))?;
94
95        match response.status() {
96            StatusCode::OK => Ok(response
97                .json::<Value>()
98                .await
99                .map_err(|e| ServerError::InvalidResponse(e.into()))?),
100            status => Err(ServerError::InvalidRequest(anyhow::anyhow!(
101                "HTTP request returned unexpected status: {status}"
102            ))),
103        }
104    }
105}