1use crate::rpc::{JsonRpcResponse, RpcClient, RpcError};
2use std::collections::HashMap;
3
4const MAX_RESPONSE_SIZE: usize = 10 * 1024 * 1024; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
8pub struct HttpRpcRequest {
9 pub url: String,
10 pub options: HttpOptions,
11}
12
13#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
14#[serde(untagged)]
15pub enum HttpBody {
16 Text(String),
17 Binary(Vec<u8>),
18 Form(HashMap<String, String>),
19 Multipart(Vec<MultipartField>),
20}
21
22#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
23pub struct HttpOptions {
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub method: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub headers: Option<HashMap<String, String>>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub body: Option<HttpBody>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub timeout: Option<u32>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub query_params: Option<HashMap<String, String>>,
34}
35
36impl Default for HttpOptions {
37 fn default() -> Self {
38 Self {
39 method: Some("GET".to_string()),
40 headers: None,
41 body: None,
42 timeout: Some(30000), query_params: None,
44 }
45 }
46}
47
48impl HttpOptions {
49 pub fn new() -> Self {
50 Self::default()
51 }
52
53 pub fn method<S: Into<String>>(mut self, method: S) -> Self {
54 self.method = Some(method.into());
55 self
56 }
57
58 pub fn header<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
59 if self.headers.is_none() {
60 self.headers = Some(HashMap::new());
61 }
62 self.headers
63 .as_mut()
64 .unwrap()
65 .insert(key.into(), value.into());
66 self
67 }
68
69 pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
70 self.headers = Some(headers);
71 self
72 }
73
74 pub fn body<S: Into<String>>(mut self, body: S) -> Self {
75 self.body = Some(HttpBody::Text(body.into()));
76 self
77 }
78
79 pub fn body_binary(mut self, data: Vec<u8>) -> Self {
80 self.body = Some(HttpBody::Binary(data));
81 self
82 }
83
84 pub fn form(mut self, form_data: HashMap<String, String>) -> Self {
85 self.body = Some(HttpBody::Form(form_data));
86 self = self.header("Content-Type", "application/x-www-form-urlencoded");
87 self
88 }
89
90 pub fn multipart(mut self, fields: Vec<MultipartField>) -> Self {
91 self.body = Some(HttpBody::Multipart(fields));
92 self
94 }
95
96 pub fn timeout(mut self, timeout_ms: u32) -> Self {
97 self.timeout = Some(timeout_ms);
98 self
99 }
100
101 pub fn json<T: serde::Serialize>(mut self, data: &T) -> Result<Self, HttpError> {
102 let json_body = serde_json::to_string(data).map_err(|_| HttpError::SerializationError)?;
103 self.body = Some(HttpBody::Text(json_body));
104 self = self.header("Content-Type", "application/json");
105 Ok(self)
106 }
107
108 pub fn basic_auth<U: Into<String>, P: Into<String>>(self, username: U, password: P) -> Self {
109 let credentials = format!("{}:{}", username.into(), password.into());
110 let encoded = base64::encode_config(credentials.as_bytes(), base64::STANDARD);
111 self.header("Authorization", format!("Basic {}", encoded))
112 }
113
114 pub fn bearer_auth<T: Into<String>>(self, token: T) -> Self {
115 self.header("Authorization", format!("Bearer {}", token.into()))
116 }
117
118 pub fn query_param<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
119 if self.query_params.is_none() {
120 self.query_params = Some(HashMap::new());
121 }
122 self.query_params
123 .as_mut()
124 .unwrap()
125 .insert(key.into(), value.into());
126 self
127 }
128
129 pub fn query_params(mut self, params: HashMap<String, String>) -> Self {
130 self.query_params = Some(params);
131 self
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
136pub enum MultipartValue {
137 Text(String),
138 Binary {
139 data: Vec<u8>,
140 filename: Option<String>,
141 content_type: Option<String>,
142 },
143}
144
145#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
146pub struct MultipartField {
147 pub name: String,
148 pub value: MultipartValue,
149}
150
151impl MultipartField {
152 pub fn text<N: Into<String>, V: Into<String>>(name: N, value: V) -> Self {
153 Self {
154 name: name.into(),
155 value: MultipartValue::Text(value.into()),
156 }
157 }
158
159 pub fn binary<N: Into<String>>(
160 name: N,
161 data: Vec<u8>,
162 filename: Option<String>,
163 content_type: Option<String>,
164 ) -> Self {
165 Self {
166 name: name.into(),
167 value: MultipartValue::Binary {
168 data,
169 filename,
170 content_type,
171 },
172 }
173 }
174
175 pub fn file<N: Into<String>, F: Into<String>>(
176 name: N,
177 data: Vec<u8>,
178 filename: F,
179 content_type: Option<String>,
180 ) -> Self {
181 Self {
182 name: name.into(),
183 value: MultipartValue::Binary {
184 data,
185 filename: Some(filename.into()),
186 content_type,
187 },
188 }
189 }
190}
191
192#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
193pub struct HttpResponse {
194 pub status: u16,
195 pub headers: HashMap<String, String>,
196 pub body: Vec<u8>,
197 pub url: String,
198}
199
200impl HttpResponse {
201 pub fn text(&self) -> Result<String, HttpError> {
202 String::from_utf8(self.body.clone()).map_err(|_| HttpError::Utf8Error)
203 }
204
205 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, HttpError> {
206 let text = self.text()?;
207 serde_json::from_str(&text).map_err(|_| HttpError::JsonParseError)
208 }
209
210 pub fn bytes(&self) -> &[u8] {
211 &self.body
212 }
213
214 pub fn status(&self) -> u16 {
215 self.status
216 }
217
218 pub fn is_success(&self) -> bool {
219 self.status >= 200 && self.status < 300
220 }
221
222 pub fn headers(&self) -> &HashMap<String, String> {
223 &self.headers
224 }
225
226 pub fn header(&self, name: &str) -> Option<&String> {
227 self.headers.get(name)
228 }
229
230 pub fn url(&self) -> &str {
231 &self.url
232 }
233}
234
235#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
236pub struct HttpResult {
237 pub success: bool,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub data: Option<HttpResponse>,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub error: Option<String>,
242}
243
244#[derive(Debug, Clone)]
245pub enum HttpError {
246 InvalidUrl,
247 SerializationError,
248 JsonParseError,
249 Utf8Error,
250 EmptyResponse,
251 RequestFailed(String),
252 NetworkError,
253 Timeout,
254 RpcError(RpcError),
255 Unknown(u32),
256}
257
258impl std::fmt::Display for HttpError {
259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260 match self {
261 HttpError::InvalidUrl => write!(f, "Invalid URL provided"),
262 HttpError::SerializationError => write!(f, "Failed to serialize request data"),
263 HttpError::JsonParseError => write!(f, "Failed to parse JSON response"),
264 HttpError::Utf8Error => write!(f, "Invalid UTF-8 in response"),
265 HttpError::EmptyResponse => write!(f, "Empty response received"),
266 HttpError::RequestFailed(msg) => write!(f, "Request failed: {}", msg),
267 HttpError::NetworkError => write!(f, "Network error occurred"),
268 HttpError::Timeout => write!(f, "Request timed out"),
269 HttpError::RpcError(e) => write!(f, "RPC error: {}", e),
270 HttpError::Unknown(code) => write!(f, "Unknown error (code: {})", code),
271 }
272 }
273}
274
275impl From<RpcError> for HttpError {
276 fn from(e: RpcError) -> Self {
277 HttpError::RpcError(e)
278 }
279}
280
281impl std::error::Error for HttpError {}
282
283pub struct HttpClient {
284 default_headers: Option<HashMap<String, String>>,
285 timeout: Option<u32>,
286}
287
288impl Default for HttpClient {
289 fn default() -> Self {
290 Self::new()
291 }
292}
293
294impl Clone for HttpClient {
295 fn clone(&self) -> Self {
296 Self {
297 default_headers: self.default_headers.clone(),
298 timeout: self.timeout,
299 }
300 }
301}
302
303impl HttpClient {
304 pub fn new() -> Self {
305 Self {
306 default_headers: None,
307 timeout: Some(30000), }
309 }
310
311 pub fn builder() -> HttpClientBuilder {
312 HttpClientBuilder::new()
313 }
314
315 pub fn get<U: Into<String>>(&self, url: U) -> RequestBuilder {
317 self.request("GET", url)
318 }
319
320 pub fn post<U: Into<String>>(&self, url: U) -> RequestBuilder {
321 self.request("POST", url)
322 }
323
324 pub fn put<U: Into<String>>(&self, url: U) -> RequestBuilder {
325 self.request("PUT", url)
326 }
327
328 pub fn patch<U: Into<String>>(&self, url: U) -> RequestBuilder {
329 self.request("PATCH", url)
330 }
331
332 pub fn delete<U: Into<String>>(&self, url: U) -> RequestBuilder {
333 self.request("DELETE", url)
334 }
335
336 pub fn head<U: Into<String>>(&self, url: U) -> RequestBuilder {
337 self.request("HEAD", url)
338 }
339
340 pub fn request<U: Into<String>>(&self, method: &str, url: U) -> RequestBuilder {
341 let mut headers = HashMap::new();
342 if let Some(ref default_headers) = self.default_headers {
343 headers.extend(default_headers.clone());
344 }
345
346 RequestBuilder {
347 client: self.clone(),
348 method: method.to_string(),
349 url: url.into(),
350 headers,
351 query_params: HashMap::new(),
352 body: None,
353 timeout: self.timeout,
354 }
355 }
356
357 fn execute(&self, builder: &RequestBuilder) -> Result<HttpResponse, HttpError> {
358 let options = HttpOptions {
359 method: Some(builder.method.clone()),
360 headers: if builder.headers.is_empty() {
361 None
362 } else {
363 Some(builder.headers.clone())
364 },
365 body: builder.body.clone(),
366 timeout: builder.timeout,
367 query_params: if builder.query_params.is_empty() {
368 None
369 } else {
370 Some(builder.query_params.clone())
371 },
372 };
373
374 self.make_request(&builder.url, options)
375 }
376
377 fn make_request(&self, url: &str, options: HttpOptions) -> Result<HttpResponse, HttpError> {
378 if url.is_empty() {
379 return Err(HttpError::InvalidUrl);
380 }
381
382 let final_url = if let Some(ref params) = options.query_params {
384 build_url_with_params(url, params)
385 } else {
386 url.to_string()
387 };
388
389 let request = HttpRpcRequest {
390 url: final_url,
391 options,
392 };
393 let mut rpc_client = RpcClient::with_buffer_size(MAX_RESPONSE_SIZE);
394 let response: JsonRpcResponse<HttpResult> =
395 rpc_client.call("http.request", Some(request))?;
396
397 if let Some(error) = response.error {
398 return Err(HttpError::RequestFailed(format!(
399 "RPC error: {} (code: {})",
400 error.message, error.code
401 )));
402 }
403 let http_result = response.result.ok_or(HttpError::EmptyResponse)?;
404
405 if !http_result.success {
406 let error_msg = http_result
407 .error
408 .unwrap_or_else(|| "Unknown error".to_string());
409 return Err(HttpError::RequestFailed(error_msg));
410 }
411
412 http_result.data.ok_or(HttpError::EmptyResponse)
413 }
414}
415
416pub struct HttpClientBuilder {
417 default_headers: Option<HashMap<String, String>>,
418 timeout: Option<u32>,
419}
420
421impl Default for HttpClientBuilder {
422 fn default() -> Self {
423 Self::new()
424 }
425}
426
427impl HttpClientBuilder {
428 pub fn new() -> Self {
429 Self {
430 default_headers: None,
431 timeout: Some(30000),
432 }
433 }
434
435 pub fn default_headers(mut self, headers: HashMap<String, String>) -> Self {
436 self.default_headers = Some(headers);
437 self
438 }
439
440 pub fn timeout(mut self, timeout: u32) -> Self {
441 self.timeout = Some(timeout);
442 self
443 }
444
445 pub fn build(self) -> HttpClient {
446 HttpClient {
447 default_headers: self.default_headers,
448 timeout: self.timeout,
449 }
450 }
451}
452pub struct RequestBuilder {
453 client: HttpClient,
454 method: String,
455 url: String,
456 headers: HashMap<String, String>,
457 query_params: HashMap<String, String>,
458 body: Option<HttpBody>,
459 timeout: Option<u32>,
460}
461
462impl RequestBuilder {
463 pub fn header<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
464 self.headers.insert(key.into(), value.into());
465 self
466 }
467
468 pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
469 self.headers.extend(headers);
470 self
471 }
472
473 pub fn query<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
474 self.query_params.insert(key.into(), value.into());
475 self
476 }
477
478 pub fn query_params(mut self, params: HashMap<String, String>) -> Self {
479 self.query_params.extend(params);
480 self
481 }
482
483 pub fn basic_auth<U: Into<String>, P: Into<String>>(
484 mut self,
485 username: U,
486 password: P,
487 ) -> Self {
488 let credentials = format!("{}:{}", username.into(), password.into());
489 let encoded = base64::encode_config(credentials.as_bytes(), base64::STANDARD);
490 self.headers
491 .insert("Authorization".to_string(), format!("Basic {}", encoded));
492 self
493 }
494
495 pub fn bearer_auth<T: Into<String>>(mut self, token: T) -> Self {
496 self.headers.insert(
497 "Authorization".to_string(),
498 format!("Bearer {}", token.into()),
499 );
500 self
501 }
502
503 pub fn timeout(mut self, timeout: u32) -> Self {
504 self.timeout = Some(timeout);
505 self
506 }
507
508 pub fn body<S: Into<String>>(mut self, body: S) -> Self {
509 self.body = Some(HttpBody::Text(body.into()));
510 self
511 }
512
513 pub fn body_bytes(mut self, body: Vec<u8>) -> Self {
514 self.body = Some(HttpBody::Binary(body));
515 self
516 }
517
518 pub fn form(mut self, form: HashMap<String, String>) -> Self {
519 self.body = Some(HttpBody::Form(form));
520 self.headers.insert(
521 "Content-Type".to_string(),
522 "application/x-www-form-urlencoded".to_string(),
523 );
524 self
525 }
526
527 pub fn multipart(mut self, form: Vec<MultipartField>) -> Self {
528 self.body = Some(HttpBody::Multipart(form));
529 self
530 }
531
532 pub fn json<T: serde::Serialize>(mut self, json: &T) -> Result<Self, HttpError> {
533 let json_body = serde_json::to_string(json).map_err(|_| HttpError::SerializationError)?;
534 self.body = Some(HttpBody::Text(json_body));
535 self.headers
536 .insert("Content-Type".to_string(), "application/json".to_string());
537 Ok(self)
538 }
539
540 pub fn send(self) -> Result<HttpResponse, HttpError> {
541 self.client.execute(&self)
542 }
543}
544
545pub fn build_url_with_params(base_url: &str, params: &HashMap<String, String>) -> String {
550 if params.is_empty() {
551 return base_url.to_string();
552 }
553 match url::Url::parse(base_url) {
554 Ok(mut url) => {
555 for (key, value) in params {
556 url.query_pairs_mut().append_pair(key, value);
557 }
558 url.to_string()
559 }
560 Err(_) => {
561 let mut url = base_url.to_string();
563 let separator = if url.contains('?') { '&' } else { '?' };
564 url.push(separator);
565
566 let encoded_params: Vec<String> = params
567 .iter()
568 .map(|(k, v)| {
569 format!(
570 "{}={}",
571 url::form_urlencoded::byte_serialize(k.as_bytes()).collect::<String>(),
572 url::form_urlencoded::byte_serialize(v.as_bytes()).collect::<String>()
573 )
574 })
575 .collect();
576 url.push_str(&encoded_params.join("&"));
577 url
578 }
579 }
580}
581
582pub fn get<U: Into<String>>(url: U) -> RequestBuilder {
587 HttpClient::new().get(url)
588}
589
590pub fn post<U: Into<String>>(url: U) -> RequestBuilder {
591 HttpClient::new().post(url)
592}
593
594pub fn put<U: Into<String>>(url: U) -> RequestBuilder {
595 HttpClient::new().put(url)
596}
597
598pub fn patch<U: Into<String>>(url: U) -> RequestBuilder {
599 HttpClient::new().patch(url)
600}
601
602pub fn delete<U: Into<String>>(url: U) -> RequestBuilder {
603 HttpClient::new().delete(url)
604}
605
606pub fn head<U: Into<String>>(url: U) -> RequestBuilder {
607 HttpClient::new().head(url)
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613
614 #[test]
615 fn test_deserialize_array_body() {
616 let json_str = r#"{"success":true,"data":{"status":200,"headers":{"content-type":"application/json"},"body":[123,34,104,101,108,108,111,34,58,34,119,111,114,108,100,34,125],"url":"https://httpbin.org/get"}}"#;
617
618 let result: HttpResult = serde_json::from_str(json_str).unwrap();
619 assert!(result.success);
620
621 let response = result.data.unwrap();
622 assert_eq!(response.status, 200);
623
624 let expected_body = b"{\"hello\":\"world\"}";
626 assert_eq!(response.body, expected_body);
627
628 let body_text = response.text().unwrap();
629 assert_eq!(body_text, "{\"hello\":\"world\"}");
630 }
631
632 #[test]
633 fn test_multipart_field_creation() {
634 let text_field = MultipartField::text("name", "value");
635 assert_eq!(text_field.name, "name");
636 match text_field.value {
637 MultipartValue::Text(ref v) => assert_eq!(v, "value"),
638 _ => panic!("Expected text value"),
639 }
640
641 let binary_field =
642 MultipartField::binary("file", vec![1, 2, 3], Some("test.bin".to_string()), None);
643 assert_eq!(binary_field.name, "file");
644 match binary_field.value {
645 MultipartValue::Binary {
646 ref data,
647 ref filename,
648 ..
649 } => {
650 assert_eq!(data, &vec![1, 2, 3]);
651 assert_eq!(filename.as_ref().unwrap(), "test.bin");
652 }
653 _ => panic!("Expected binary value"),
654 }
655 }
656
657 #[test]
658 fn test_url_building() {
659 let mut params = HashMap::new();
660 params.insert("key1".to_string(), "value1".to_string());
661 params.insert("key2".to_string(), "value with spaces".to_string());
662
663 let url = build_url_with_params("https://example.com/api", ¶ms);
664 assert!(url.contains("key1=value1"));
665 assert!(url.contains("key2=value+with+spaces"));
666 assert!(url.starts_with("https://example.com/api?"));
667 }
668
669 #[test]
670 fn test_url_building_special_chars() {
671 let mut params = HashMap::new();
672 params.insert("special".to_string(), "!@#$%^&*()".to_string());
673 params.insert("utf8".to_string(), "こんにちは".to_string());
674 params.insert("reserved".to_string(), "test&foo=bar".to_string());
675
676 let url = build_url_with_params("https://example.com/api", ¶ms);
677
678 assert!(url.contains("special=%21%40%23%24%25%5E%26*%28%29"));
681 assert!(url.contains("reserved=test%26foo%3Dbar"));
682 assert!(url.contains("utf8=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF"));
684 }
685
686 #[test]
687 fn test_url_building_with_existing_query() {
688 let mut params = HashMap::new();
689 params.insert("new_param".to_string(), "new_value".to_string());
690
691 let url = build_url_with_params("https://example.com/api?existing=param", ¶ms);
692 assert!(url.contains("existing=param"));
693 assert!(url.contains("new_param=new_value"));
694 assert!(url.contains("&"));
695 }
696
697 #[test]
698 fn test_url_building_empty_params() {
699 let params = HashMap::new();
700 let url = build_url_with_params("https://example.com/api", ¶ms);
701 assert_eq!(url, "https://example.com/api");
702 }
703
704 #[test]
705 fn test_client_builder() {
706 let mut headers = HashMap::new();
707 headers.insert("User-Agent".to_string(), "Blockless-SDK/1.0".to_string());
708
709 let client = HttpClient::builder()
710 .default_headers(headers)
711 .timeout(10000)
712 .build();
713
714 assert!(client.default_headers.is_some());
715 assert_eq!(client.timeout, Some(10000));
716 }
717
718 #[test]
719 fn test_request_builder() {
720 let client = HttpClient::new();
721 let request = client
722 .post("https://httpbin.org/post")
723 .header("Content-Type", "application/json")
724 .query("search", "test")
725 .query("limit", "10")
726 .body("test body")
727 .timeout(5000);
728
729 assert_eq!(request.method, "POST");
730 assert_eq!(request.url, "https://httpbin.org/post");
731 assert_eq!(
732 request.headers.get("Content-Type").unwrap(),
733 "application/json"
734 );
735 assert_eq!(request.query_params.get("search").unwrap(), "test");
736 assert_eq!(request.query_params.get("limit").unwrap(), "10");
737 assert_eq!(request.timeout, Some(5000));
738
739 match request.body.as_ref().unwrap() {
740 HttpBody::Text(ref body) => assert_eq!(body, "test body"),
741 _ => panic!("Expected text body"),
742 }
743 }
744
745 #[test]
746 fn test_basic_auth() {
747 let client = HttpClient::new();
748 let request = client
749 .get("https://httpbin.org/basic-auth/user/pass")
750 .basic_auth("username", "password");
751
752 let auth_header = request.headers.get("Authorization").unwrap();
753 assert!(auth_header.starts_with("Basic "));
754
755 let encoded_part = &auth_header[6..]; let decoded = base64::decode_config(encoded_part, base64::STANDARD).unwrap();
758 let decoded_str = String::from_utf8(decoded).unwrap();
759 assert_eq!(decoded_str, "username:password");
760 }
761
762 #[test]
763 fn test_bearer_auth() {
764 let client = HttpClient::new();
765 let request = client
766 .get("https://httpbin.org/bearer")
767 .bearer_auth("test-token-123");
768
769 let auth_header = request.headers.get("Authorization").unwrap();
770 assert_eq!(auth_header, "Bearer test-token-123");
771 }
772
773 #[test]
774 fn test_query_params_integration() {
775 let mut params1 = HashMap::new();
776 params1.insert("base".to_string(), "param".to_string());
777
778 let client = HttpClient::new();
779 let request = client
780 .get("https://api.example.com/search")
781 .query_params(params1)
782 .query("additional", "value")
783 .query("special chars", "test & encode");
784
785 assert_eq!(request.query_params.get("base").unwrap(), "param");
786 assert_eq!(request.query_params.get("additional").unwrap(), "value");
787 assert_eq!(
788 request.query_params.get("special chars").unwrap(),
789 "test & encode"
790 );
791
792 let url = build_url_with_params("https://api.example.com/search", &request.query_params);
794 assert!(url.contains("base=param"));
795 assert!(url.contains("additional=value"));
796 assert!(url.contains("special+chars=test+%26+encode"));
797 }
798
799 #[test]
800 fn test_module_level_functions() {
801 let _get_request = get("https://httpbin.org/get");
803 let _post_request = post("https://httpbin.org/post");
804 let _put_request = put("https://httpbin.org/put");
805 let _patch_request = patch("https://httpbin.org/patch");
806 let _delete_request = delete("https://httpbin.org/delete");
807
808 let request = get("https://httpbin.org/get")
810 .query("test", "value")
811 .header("User-Agent", "test");
812
813 assert_eq!(request.method, "GET");
814 assert_eq!(request.url, "https://httpbin.org/get");
815 assert_eq!(request.query_params.get("test").unwrap(), "value");
816 assert_eq!(request.headers.get("User-Agent").unwrap(), "test");
817 }
818}