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 no_redirects: bool,
154 #[cfg(not(target_arch = "wasm32"))]
155 proxy: Option<ProxyConfig>,
156}
157
158#[cfg(not(target_arch = "wasm32"))]
159#[derive(Debug)]
160struct ProxyConfig {
161 url: url::Url,
162 matcher: Option<regex::Regex>,
163}
164
165impl HttpClientBuilder {
166 #[cfg(not(target_arch = "wasm32"))]
168 pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
169 self.accept_invalid_certs = accept;
170 self
171 }
172
173 #[cfg(not(target_arch = "wasm32"))]
175 pub fn proxy(mut self, url: url::Url) -> Self {
176 self.proxy = Some(ProxyConfig { url, matcher: None });
177 self
178 }
179
180 #[cfg(not(target_arch = "wasm32"))]
182 pub fn proxy_with_matcher(mut self, url: url::Url, pattern: &str) -> Response<Self> {
183 let matcher = regex::Regex::new(pattern)
184 .map_err(|e| HttpError::Proxy(format!("Invalid proxy pattern: {}", e)))?;
185 self.proxy = Some(ProxyConfig {
186 url,
187 matcher: Some(matcher),
188 });
189 Ok(self)
190 }
191
192 #[cfg(not(target_arch = "wasm32"))]
194 pub fn no_redirects(mut self) -> Self {
195 self.no_redirects = true;
196 self
197 }
198
199 pub fn build(self) -> Response<HttpClient> {
201 #[cfg(not(target_arch = "wasm32"))]
202 {
203 let mut builder =
204 reqwest::Client::builder().danger_accept_invalid_certs(self.accept_invalid_certs);
205
206 if self.no_redirects {
207 builder = builder.redirect(reqwest::redirect::Policy::none());
208 }
209
210 if let Some(proxy_config) = self.proxy {
211 let proxy_url = proxy_config.url.to_string();
212 let proxy = if let Some(matcher) = proxy_config.matcher {
213 reqwest::Proxy::custom(move |url| {
214 if matcher.is_match(url.host_str().unwrap_or("")) {
215 Some(proxy_url.clone())
216 } else {
217 None
218 }
219 })
220 } else {
221 reqwest::Proxy::all(&proxy_url).map_err(|e| HttpError::Proxy(e.to_string()))?
222 };
223 builder = builder.proxy(proxy);
224 }
225
226 let client = builder.build().map_err(HttpError::from)?;
227 Ok(HttpClient { inner: client })
228 }
229
230 #[cfg(target_arch = "wasm32")]
231 {
232 Ok(HttpClient::new())
233 }
234 }
235}
236
237pub async fn fetch<R: DeserializeOwned>(url: &str) -> Response<R> {
239 HttpClient::new().fetch(url).await
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn test_client_new() {
248 let client = HttpClient::new();
249 let _ = format!("{:?}", client);
251 }
252
253 #[test]
254 fn test_client_default() {
255 let client = HttpClient::default();
256 let _ = format!("{:?}", client);
258 }
259
260 #[test]
261 fn test_builder_returns_builder() {
262 let builder = HttpClient::builder();
263 let _ = format!("{:?}", builder);
264 }
265
266 #[test]
267 fn test_builder_build() {
268 let result = HttpClientBuilder::default().build();
269 assert!(result.is_ok());
270 }
271
272 #[test]
273 fn test_from_reqwest() {
274 let reqwest_client = reqwest::Client::new();
275 let client = HttpClient::from_reqwest(reqwest_client);
276 let _ = format!("{:?}", client);
277 }
278
279 #[cfg(not(target_arch = "wasm32"))]
280 mod non_wasm {
281 use super::*;
282
283 #[test]
284 fn test_builder_accept_invalid_certs() {
285 let result = HttpClientBuilder::default()
286 .danger_accept_invalid_certs(true)
287 .build();
288 assert!(result.is_ok());
289 }
290
291 #[test]
292 fn test_builder_accept_invalid_certs_false() {
293 let result = HttpClientBuilder::default()
294 .danger_accept_invalid_certs(false)
295 .build();
296 assert!(result.is_ok());
297 }
298
299 #[test]
300 fn test_builder_no_redirects() {
301 let result = HttpClientBuilder::default().no_redirects().build();
302 assert!(result.is_ok());
303 }
304
305 #[test]
306 fn test_builder_proxy() {
307 let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
308 let result = HttpClientBuilder::default().proxy(proxy_url).build();
309 assert!(result.is_ok());
310 }
311
312 #[test]
313 fn test_builder_proxy_with_valid_matcher() {
314 let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
315 let result =
316 HttpClientBuilder::default().proxy_with_matcher(proxy_url, r".*\.example\.com$");
317 assert!(result.is_ok());
318
319 let builder = result.expect("Valid matcher should succeed");
320 let client_result = builder.build();
321 assert!(client_result.is_ok());
322 }
323
324 #[test]
325 fn test_builder_proxy_with_invalid_matcher() {
326 let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
327 let result = HttpClientBuilder::default().proxy_with_matcher(proxy_url, r"[invalid");
329 assert!(result.is_err());
330
331 if let Err(HttpError::Proxy(msg)) = result {
332 assert!(msg.contains("Invalid proxy pattern"));
333 } else {
334 panic!("Expected HttpError::Proxy");
335 }
336 }
337
338 #[test]
339 fn test_builder_chained_config() {
340 let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
341 let result = HttpClientBuilder::default()
342 .danger_accept_invalid_certs(true)
343 .proxy(proxy_url)
344 .build();
345 assert!(result.is_ok());
346 }
347 }
348}