xjp_oidc/
http.rs

1//! HTTP client abstraction for both server and WASM environments
2
3use async_trait::async_trait;
4use thiserror::Error;
5
6/// HTTP client errors
7#[derive(Error, Debug)]
8pub enum HttpClientError {
9    /// Network request failed
10    #[error("request failed: {0}")]
11    RequestFailed(String),
12    /// Response parsing failed
13    #[error("response parse error: {0}")]
14    ParseError(String),
15    /// Invalid response status
16    #[error("invalid status: {status} - {message}")]
17    InvalidStatus {
18        /// HTTP status code
19        status: u16,
20        /// Error message from response
21        message: String,
22    },
23    /// Timeout occurred
24    #[error("request timeout")]
25    Timeout,
26    /// Not supported in current environment
27    #[error("operation not supported: {0}")]
28    NotSupported(String),
29}
30
31/// HTTP client trait for abstraction over different implementations
32///
33/// This trait uses serde_json::Value to maintain object safety
34#[async_trait]
35pub trait HttpClient: Send + Sync {
36    /// Perform a GET request and return JSON value
37    async fn get_value(&self, url: &str) -> Result<serde_json::Value, HttpClientError>;
38
39    /// Perform a POST request with form data and return JSON value
40    async fn post_form_value(
41        &self,
42        url: &str,
43        form: &[(String, String)],
44        auth_header: Option<(&str, &str)>,
45    ) -> Result<serde_json::Value, HttpClientError>;
46
47    /// Perform a POST request with JSON body and return JSON value
48    async fn post_json_value(
49        &self,
50        url: &str,
51        body: &serde_json::Value,
52        auth_header: Option<(&str, &str)>,
53    ) -> Result<serde_json::Value, HttpClientError>;
54}
55
56// Server-side implementation using reqwest
57#[cfg(all(not(target_arch = "wasm32"), feature = "http-reqwest"))]
58pub use reqwest_impl::ReqwestHttpClient;
59
60#[cfg(all(not(target_arch = "wasm32"), feature = "http-reqwest"))]
61mod reqwest_impl {
62    use super::*;
63    use reqwest::Client;
64    use std::time::Duration;
65
66    /// Reqwest-based HTTP client for server environments
67    #[derive(Clone)]
68    pub struct ReqwestHttpClient {
69        client: Client,
70    }
71
72    impl ReqwestHttpClient {
73        /// Create a new HTTP client with default settings
74        pub fn new() -> Result<Self, HttpClientError> {
75            let client = Client::builder()
76                .timeout(Duration::from_secs(30))
77                .use_rustls_tls()
78                .cookie_store(true)
79                .build()
80                .map_err(|e| HttpClientError::RequestFailed(e.to_string()))?;
81
82            Ok(Self { client })
83        }
84
85        /// Create a new HTTP client with custom timeout
86        pub fn with_timeout(timeout_secs: u64) -> Result<Self, HttpClientError> {
87            let client = Client::builder()
88                .timeout(Duration::from_secs(timeout_secs))
89                .use_rustls_tls()
90                .cookie_store(true)
91                .build()
92                .map_err(|e| HttpClientError::RequestFailed(e.to_string()))?;
93
94            Ok(Self { client })
95        }
96    }
97
98    impl Default for ReqwestHttpClient {
99        fn default() -> Self {
100            Self::new().expect("Failed to create default HTTP client")
101        }
102    }
103
104    #[async_trait]
105    impl HttpClient for ReqwestHttpClient {
106        async fn get_value(&self, url: &str) -> Result<serde_json::Value, HttpClientError> {
107            let start = std::time::Instant::now();
108            let response = self.client.get(url).send().await.map_err(|e| {
109                tracing::error!(
110                    target: "xjp_oidc::http",
111                    url = %url,
112                    error = %e,
113                    "HTTP GET 请求失败"
114                );
115                if e.is_timeout() {
116                    HttpClientError::Timeout
117                } else {
118                    HttpClientError::RequestFailed(e.to_string())
119                }
120            })?;
121
122            let status = response.status();
123            let duration = start.elapsed();
124
125            tracing::info!(
126                target: "xjp_oidc::http",
127                url = %url,
128                method = "GET",
129                duration_ms = duration.as_millis() as u64,
130                status = status.as_u16(),
131                "HTTP 请求完成"
132            );
133            if !status.is_success() {
134                let message =
135                    response.text().await.unwrap_or_else(|_| "No error message".to_string());
136                return Err(HttpClientError::InvalidStatus { status: status.as_u16(), message });
137            }
138
139            response
140                .json::<serde_json::Value>()
141                .await
142                .map_err(|e| HttpClientError::ParseError(e.to_string()))
143        }
144
145        async fn post_form_value(
146            &self,
147            url: &str,
148            form: &[(String, String)],
149            auth_header: Option<(&str, &str)>,
150        ) -> Result<serde_json::Value, HttpClientError> {
151            let mut request = self.client.post(url).form(form);
152
153            if let Some((name, value)) = auth_header {
154                request = request.header(name, value);
155            }
156
157            let response = request.send().await.map_err(|e| {
158                if e.is_timeout() {
159                    HttpClientError::Timeout
160                } else {
161                    HttpClientError::RequestFailed(e.to_string())
162                }
163            })?;
164
165            let status = response.status();
166            if !status.is_success() {
167                let message =
168                    response.text().await.unwrap_or_else(|_| "No error message".to_string());
169                return Err(HttpClientError::InvalidStatus { status: status.as_u16(), message });
170            }
171
172            response
173                .json::<serde_json::Value>()
174                .await
175                .map_err(|e| HttpClientError::ParseError(e.to_string()))
176        }
177
178        async fn post_json_value(
179            &self,
180            url: &str,
181            body: &serde_json::Value,
182            auth_header: Option<(&str, &str)>,
183        ) -> Result<serde_json::Value, HttpClientError> {
184            let mut request = self.client.post(url).json(body);
185
186            if let Some((name, value)) = auth_header {
187                request = request.header(name, value);
188            }
189
190            let response = request.send().await.map_err(|e| {
191                if e.is_timeout() {
192                    HttpClientError::Timeout
193                } else {
194                    HttpClientError::RequestFailed(e.to_string())
195                }
196            })?;
197
198            let status = response.status();
199            if !status.is_success() {
200                let message =
201                    response.text().await.unwrap_or_else(|_| "No error message".to_string());
202                return Err(HttpClientError::InvalidStatus { status: status.as_u16(), message });
203            }
204
205            response
206                .json::<serde_json::Value>()
207                .await
208                .map_err(|e| HttpClientError::ParseError(e.to_string()))
209        }
210    }
211}
212
213// WASM implementation using gloo-net
214#[cfg(all(target_arch = "wasm32", feature = "http-wasm"))]
215pub use wasm_impl::WasmHttpClient;
216
217#[cfg(all(target_arch = "wasm32", feature = "http-wasm"))]
218mod wasm_impl {
219    use super::*;
220    use gloo_net::http::Request;
221    use web_sys::RequestCredentials;
222
223    /// WASM-based HTTP client for browser environments
224    pub struct WasmHttpClient;
225
226    impl WasmHttpClient {
227        /// Create a new WASM HTTP client
228        pub fn new() -> Self {
229            Self
230        }
231    }
232
233    impl Default for WasmHttpClient {
234        fn default() -> Self {
235            Self::new()
236        }
237    }
238
239    #[async_trait(?Send)]
240    impl HttpClient for WasmHttpClient {
241        async fn get_value(&self, url: &str) -> Result<serde_json::Value, HttpClientError> {
242            let response = Request::get(url)
243                .credentials(RequestCredentials::Include)
244                .send()
245                .await
246                .map_err(|e| HttpClientError::RequestFailed(e.to_string()))?;
247
248            if !response.ok() {
249                return Err(HttpClientError::InvalidStatus {
250                    status: response.status(),
251                    message: response
252                        .text()
253                        .await
254                        .unwrap_or_else(|_| "No error message".to_string()),
255                });
256            }
257
258            response
259                .json::<serde_json::Value>()
260                .await
261                .map_err(|e| HttpClientError::ParseError(e.to_string()))
262        }
263
264        async fn post_form_value(
265            &self,
266            url: &str,
267            form: &[(String, String)],
268            auth_header: Option<(&str, &str)>,
269        ) -> Result<serde_json::Value, HttpClientError> {
270            let mut request = Request::post(url).credentials(RequestCredentials::Include);
271
272            if let Some((name, value)) = auth_header {
273                request = request.header(name, value);
274            }
275
276            // Convert form data to URL-encoded string
277            let form_body = form
278                .iter()
279                .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
280                .collect::<Vec<_>>()
281                .join("&");
282
283            let response = request
284                .header("Content-Type", "application/x-www-form-urlencoded")
285                .body(form_body)
286                .send()
287                .await
288                .map_err(|e| HttpClientError::RequestFailed(e.to_string()))?;
289
290            if !response.ok() {
291                return Err(HttpClientError::InvalidStatus {
292                    status: response.status(),
293                    message: response
294                        .text()
295                        .await
296                        .unwrap_or_else(|_| "No error message".to_string()),
297                });
298            }
299
300            response
301                .json::<serde_json::Value>()
302                .await
303                .map_err(|e| HttpClientError::ParseError(e.to_string()))
304        }
305
306        async fn post_json_value(
307            &self,
308            url: &str,
309            body: &serde_json::Value,
310            auth_header: Option<(&str, &str)>,
311        ) -> Result<serde_json::Value, HttpClientError> {
312            let mut request = Request::post(url).credentials(RequestCredentials::Include);
313
314            if let Some((name, value)) = auth_header {
315                request = request.header(name, value);
316            }
317
318            let response = request
319                .json(body)
320                .map_err(|e| HttpClientError::RequestFailed(e.to_string()))?
321                .send()
322                .await
323                .map_err(|e| HttpClientError::RequestFailed(e.to_string()))?;
324
325            if !response.ok() {
326                return Err(HttpClientError::InvalidStatus {
327                    status: response.status(),
328                    message: response
329                        .text()
330                        .await
331                        .unwrap_or_else(|_| "No error message".to_string()),
332                });
333            }
334
335            response
336                .json::<serde_json::Value>()
337                .await
338                .map_err(|e| HttpClientError::ParseError(e.to_string()))
339        }
340    }
341}