1use async_trait::async_trait;
4use thiserror::Error;
5
6#[derive(Error, Debug)]
8pub enum HttpClientError {
9 #[error("request failed: {0}")]
11 RequestFailed(String),
12 #[error("response parse error: {0}")]
14 ParseError(String),
15 #[error("invalid status: {status} - {message}")]
17 InvalidStatus {
18 status: u16,
20 message: String,
22 },
23 #[error("request timeout")]
25 Timeout,
26 #[error("operation not supported: {0}")]
28 NotSupported(String),
29}
30
31#[async_trait]
35pub trait HttpClient: Send + Sync {
36 async fn get_value(&self, url: &str) -> Result<serde_json::Value, HttpClientError>;
38
39 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 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#[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 #[derive(Clone)]
68 pub struct ReqwestHttpClient {
69 client: Client,
70 }
71
72 impl ReqwestHttpClient {
73 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 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#[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 pub struct WasmHttpClient;
225
226 impl WasmHttpClient {
227 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 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}