Skip to main content

fedimint_connectors/
http.rs

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