Skip to main content

auths_infra_http/
registry_client.rs

1use auths_core::ports::network::{NetworkError, RateLimitInfo, RegistryClient, RegistryResponse};
2use std::future::Future;
3use std::time::Duration;
4
5use crate::error::map_reqwest_error;
6use crate::request::{
7    build_get_request, build_post_request, execute_request, parse_response_bytes,
8};
9use crate::{default_client_builder, default_http_client};
10
11/// HTTP-backed implementation of `RegistryClient`.
12///
13/// Fetches and pushes data to a remote registry service for identity
14/// and attestation synchronization.
15///
16/// Usage:
17/// ```ignore
18/// use auths_infra_http::HttpRegistryClient;
19///
20/// let client = HttpRegistryClient::new();
21/// let data = client.fetch_registry_data("https://registry.example.com", "identities/abc").await?;
22/// ```
23pub struct HttpRegistryClient {
24    client: reqwest::Client,
25}
26
27impl HttpRegistryClient {
28    pub fn new() -> Self {
29        Self {
30            client: default_http_client(),
31        }
32    }
33
34    /// Create an `HttpRegistryClient` with explicit connect and request timeouts.
35    ///
36    /// Args:
37    /// * `connect_timeout`: Maximum time to establish a TCP connection.
38    /// * `request_timeout`: Maximum total time for the request to complete.
39    ///
40    /// Usage:
41    /// ```ignore
42    /// let client = HttpRegistryClient::new_with_timeouts(
43    ///     Duration::from_secs(30),
44    ///     Duration::from_secs(60),
45    /// );
46    /// ```
47    // INVARIANT: reqwest builder with these settings cannot fail
48    #[allow(clippy::expect_used)]
49    pub fn new_with_timeouts(connect_timeout: Duration, request_timeout: Duration) -> Self {
50        let client = default_client_builder()
51            .connect_timeout(connect_timeout)
52            .timeout(request_timeout)
53            .build()
54            .expect("failed to build HTTP client");
55        Self { client }
56    }
57}
58
59impl Default for HttpRegistryClient {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl RegistryClient for HttpRegistryClient {
66    fn fetch_registry_data(
67        &self,
68        registry_url: &str,
69        path: &str,
70    ) -> impl Future<Output = Result<Vec<u8>, NetworkError>> + Send {
71        let url = format!("{}/{}", registry_url.trim_end_matches('/'), path);
72        let request = build_get_request(&self.client, &url);
73
74        async move {
75            let response = execute_request(request, registry_url).await?;
76            parse_response_bytes(response, path).await
77        }
78    }
79
80    fn push_registry_data(
81        &self,
82        registry_url: &str,
83        path: &str,
84        data: &[u8],
85    ) -> impl Future<Output = Result<(), NetworkError>> + Send {
86        let url = format!("{}/{}", registry_url.trim_end_matches('/'), path);
87        let request = build_post_request(&self.client, &url, data.to_vec());
88
89        async move {
90            let response = execute_request(request, registry_url).await?;
91            let _ = parse_response_bytes(response, path).await?;
92            Ok(())
93        }
94    }
95
96    fn post_json(
97        &self,
98        registry_url: &str,
99        path: &str,
100        json_body: &[u8],
101    ) -> impl Future<Output = Result<RegistryResponse, NetworkError>> + Send {
102        let url = format!("{}/{}", registry_url.trim_end_matches('/'), path);
103        let request = self
104            .client
105            .post(&url)
106            .header("Content-Type", "application/json")
107            .body(json_body.to_vec());
108        let endpoint = registry_url.to_string();
109
110        async move {
111            let response = request
112                .send()
113                .await
114                .map_err(|e| map_reqwest_error(e, &endpoint))?;
115            let status = response.status().as_u16();
116            let rate_limit = extract_rate_limit_headers(&response);
117            let body = response.bytes().await.map(|b| b.to_vec()).map_err(|e| {
118                NetworkError::InvalidResponse {
119                    detail: e.to_string(),
120                }
121            })?;
122            Ok(RegistryResponse {
123                status,
124                body,
125                rate_limit,
126            })
127        }
128    }
129}
130
131fn extract_rate_limit_headers(response: &reqwest::Response) -> Option<RateLimitInfo> {
132    let headers = response.headers();
133    let limit = headers
134        .get("x-ratelimit-limit")
135        .and_then(|v| v.to_str().ok())
136        .and_then(|s| s.parse::<i32>().ok());
137    let remaining = headers
138        .get("x-ratelimit-remaining")
139        .and_then(|v| v.to_str().ok())
140        .and_then(|s| s.parse::<i32>().ok());
141    let reset = headers
142        .get("x-ratelimit-reset")
143        .and_then(|v| v.to_str().ok())
144        .and_then(|s| s.parse::<i64>().ok());
145    let tier = headers
146        .get("x-ratelimit-tier")
147        .and_then(|v| v.to_str().ok())
148        .map(String::from);
149
150    if limit.is_some() || remaining.is_some() || reset.is_some() || tier.is_some() {
151        Some(RateLimitInfo {
152            limit,
153            remaining,
154            reset,
155            tier,
156        })
157    } else {
158        None
159    }
160}