cdk_http_client/
client.rs1use serde::de::DeserializeOwned;
4use serde::Serialize;
5
6use crate::error::HttpError;
7use crate::request::RequestBuilder;
8use crate::response::{RawResponse, Response};
9
10#[derive(Debug, Clone)]
12pub struct HttpClient {
13 inner: reqwest::Client,
14}
15
16impl Default for HttpClient {
17 fn default() -> Self {
18 Self::new()
19 }
20}
21
22impl HttpClient {
23 pub fn new() -> Self {
25 Self {
26 inner: reqwest::Client::new(),
27 }
28 }
29
30 pub fn builder() -> HttpClientBuilder {
32 HttpClientBuilder::default()
33 }
34
35 pub fn from_reqwest(client: reqwest::Client) -> Self {
37 Self { inner: client }
38 }
39
40 pub async fn fetch<R>(&self, url: &str) -> Response<R>
44 where
45 R: DeserializeOwned,
46 {
47 let response = self.inner.get(url).send().await?;
48 let status = response.status();
49
50 if !status.is_success() {
51 let message = response.text().await.unwrap_or_default();
52 return Err(HttpError::Status {
53 status: status.as_u16(),
54 message,
55 });
56 }
57
58 response.json().await.map_err(HttpError::from)
59 }
60
61 pub async fn post_json<B, R>(&self, url: &str, body: &B) -> Response<R>
63 where
64 B: Serialize + ?Sized,
65 R: DeserializeOwned,
66 {
67 let response = self.inner.post(url).json(body).send().await?;
68 let status = response.status();
69
70 if !status.is_success() {
71 let message = response.text().await.unwrap_or_default();
72 return Err(HttpError::Status {
73 status: status.as_u16(),
74 message,
75 });
76 }
77
78 response.json().await.map_err(HttpError::from)
79 }
80
81 pub async fn post_form<F, R>(&self, url: &str, form: &F) -> Response<R>
83 where
84 F: Serialize + ?Sized,
85 R: DeserializeOwned,
86 {
87 let response = self.inner.post(url).form(form).send().await?;
88 let status = response.status();
89
90 if !status.is_success() {
91 let message = response.text().await.unwrap_or_default();
92 return Err(HttpError::Status {
93 status: status.as_u16(),
94 message,
95 });
96 }
97
98 response.json().await.map_err(HttpError::from)
99 }
100
101 pub async fn patch_json<B, R>(&self, url: &str, body: &B) -> Response<R>
103 where
104 B: Serialize + ?Sized,
105 R: DeserializeOwned,
106 {
107 let response = self.inner.patch(url).json(body).send().await?;
108 let status = response.status();
109
110 if !status.is_success() {
111 let message = response.text().await.unwrap_or_default();
112 return Err(HttpError::Status {
113 status: status.as_u16(),
114 message,
115 });
116 }
117
118 response.json().await.map_err(HttpError::from)
119 }
120
121 pub async fn get_raw(&self, url: &str) -> Response<RawResponse> {
125 let response = self.inner.get(url).send().await?;
126 Ok(RawResponse::new(response))
127 }
128
129 pub fn post(&self, url: &str) -> RequestBuilder {
133 RequestBuilder::new(self.inner.post(url))
134 }
135
136 pub fn get(&self, url: &str) -> RequestBuilder {
138 RequestBuilder::new(self.inner.get(url))
139 }
140
141 pub fn patch(&self, url: &str) -> RequestBuilder {
143 RequestBuilder::new(self.inner.patch(url))
144 }
145}
146
147#[derive(Debug, Default)]
149pub struct HttpClientBuilder {
150 #[cfg(not(target_arch = "wasm32"))]
151 accept_invalid_certs: bool,
152 #[cfg(not(target_arch = "wasm32"))]
153 proxy: Option<ProxyConfig>,
154}
155
156#[cfg(not(target_arch = "wasm32"))]
157#[derive(Debug)]
158struct ProxyConfig {
159 url: url::Url,
160 matcher: Option<regex::Regex>,
161}
162
163impl HttpClientBuilder {
164 #[cfg(not(target_arch = "wasm32"))]
166 pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
167 self.accept_invalid_certs = accept;
168 self
169 }
170
171 #[cfg(not(target_arch = "wasm32"))]
173 pub fn proxy(mut self, url: url::Url) -> Self {
174 self.proxy = Some(ProxyConfig { url, matcher: None });
175 self
176 }
177
178 #[cfg(not(target_arch = "wasm32"))]
180 pub fn proxy_with_matcher(mut self, url: url::Url, pattern: &str) -> Response<Self> {
181 let matcher = regex::Regex::new(pattern)
182 .map_err(|e| HttpError::Proxy(format!("Invalid proxy pattern: {}", e)))?;
183 self.proxy = Some(ProxyConfig {
184 url,
185 matcher: Some(matcher),
186 });
187 Ok(self)
188 }
189
190 pub fn build(self) -> Response<HttpClient> {
192 #[cfg(not(target_arch = "wasm32"))]
193 {
194 let mut builder =
195 reqwest::Client::builder().danger_accept_invalid_certs(self.accept_invalid_certs);
196
197 if let Some(proxy_config) = self.proxy {
198 let proxy_url = proxy_config.url.to_string();
199 let proxy = if let Some(matcher) = proxy_config.matcher {
200 reqwest::Proxy::custom(move |url| {
201 if matcher.is_match(url.host_str().unwrap_or("")) {
202 Some(proxy_url.clone())
203 } else {
204 None
205 }
206 })
207 } else {
208 reqwest::Proxy::all(&proxy_url).map_err(|e| HttpError::Proxy(e.to_string()))?
209 };
210 builder = builder.proxy(proxy);
211 }
212
213 let client = builder.build().map_err(HttpError::from)?;
214 Ok(HttpClient { inner: client })
215 }
216
217 #[cfg(target_arch = "wasm32")]
218 {
219 Ok(HttpClient::new())
220 }
221 }
222}
223
224pub async fn fetch<R: DeserializeOwned>(url: &str) -> Response<R> {
226 HttpClient::new().fetch(url).await
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_client_new() {
235 let client = HttpClient::new();
236 let _ = format!("{:?}", client);
238 }
239
240 #[test]
241 fn test_client_default() {
242 let client = HttpClient::default();
243 let _ = format!("{:?}", client);
245 }
246
247 #[test]
248 fn test_builder_returns_builder() {
249 let builder = HttpClient::builder();
250 let _ = format!("{:?}", builder);
251 }
252
253 #[test]
254 fn test_builder_build() {
255 let result = HttpClientBuilder::default().build();
256 assert!(result.is_ok());
257 }
258
259 #[test]
260 fn test_from_reqwest() {
261 let reqwest_client = reqwest::Client::new();
262 let client = HttpClient::from_reqwest(reqwest_client);
263 let _ = format!("{:?}", client);
264 }
265
266 #[cfg(not(target_arch = "wasm32"))]
267 mod non_wasm {
268 use super::*;
269
270 #[test]
271 fn test_builder_accept_invalid_certs() {
272 let result = HttpClientBuilder::default()
273 .danger_accept_invalid_certs(true)
274 .build();
275 assert!(result.is_ok());
276 }
277
278 #[test]
279 fn test_builder_accept_invalid_certs_false() {
280 let result = HttpClientBuilder::default()
281 .danger_accept_invalid_certs(false)
282 .build();
283 assert!(result.is_ok());
284 }
285
286 #[test]
287 fn test_builder_proxy() {
288 let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
289 let result = HttpClientBuilder::default().proxy(proxy_url).build();
290 assert!(result.is_ok());
291 }
292
293 #[test]
294 fn test_builder_proxy_with_valid_matcher() {
295 let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
296 let result =
297 HttpClientBuilder::default().proxy_with_matcher(proxy_url, r".*\.example\.com$");
298 assert!(result.is_ok());
299
300 let builder = result.expect("Valid matcher should succeed");
301 let client_result = builder.build();
302 assert!(client_result.is_ok());
303 }
304
305 #[test]
306 fn test_builder_proxy_with_invalid_matcher() {
307 let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
308 let result = HttpClientBuilder::default().proxy_with_matcher(proxy_url, r"[invalid");
310 assert!(result.is_err());
311
312 if let Err(HttpError::Proxy(msg)) = result {
313 assert!(msg.contains("Invalid proxy pattern"));
314 } else {
315 panic!("Expected HttpError::Proxy");
316 }
317 }
318
319 #[test]
320 fn test_builder_chained_config() {
321 let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
322 let result = HttpClientBuilder::default()
323 .danger_accept_invalid_certs(true)
324 .proxy(proxy_url)
325 .build();
326 assert!(result.is_ok());
327 }
328 }
329}