1use runtara_agent_macro::{CapabilityInput, CapabilityOutput, capability};
15use runtara_dsl::agent_meta::EnumVariants;
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use std::collections::HashMap;
19use strum::VariantNames;
20
21#[derive(Debug, Clone, Serialize, Deserialize, VariantNames)]
27#[serde(rename_all = "UPPERCASE")]
28#[strum(serialize_all = "UPPERCASE")]
29pub enum HttpMethod {
30 Get,
32 Post,
34 Put,
36 Delete,
38 Patch,
40 Head,
42 Options,
44}
45
46impl EnumVariants for HttpMethod {
47 fn variant_names() -> &'static [&'static str] {
48 Self::VARIANTS
49 }
50}
51
52impl Default for HttpMethod {
53 fn default() -> Self {
54 Self::Get
55 }
56}
57
58impl HttpMethod {
59 pub fn as_str(&self) -> &str {
60 match self {
61 Self::Get => "GET",
62 Self::Post => "POST",
63 Self::Put => "PUT",
64 Self::Delete => "DELETE",
65 Self::Patch => "PATCH",
66 Self::Head => "HEAD",
67 Self::Options => "OPTIONS",
68 }
69 }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, VariantNames)]
74#[serde(rename_all = "lowercase")]
75#[strum(serialize_all = "lowercase")]
76pub enum ResponseType {
77 Json,
79 Text,
81 Binary,
83}
84
85impl EnumVariants for ResponseType {
86 fn variant_names() -> &'static [&'static str] {
87 Self::VARIANTS
88 }
89}
90
91impl Default for ResponseType {
92 fn default() -> Self {
93 Self::Json
94 }
95}
96
97impl ResponseType {
98 pub fn as_str(&self) -> &str {
99 match self {
100 Self::Json => "json",
101 Self::Text => "text",
102 Self::Binary => "binary",
103 }
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, Default)]
116#[serde(transparent)]
117pub struct HttpBody(pub Value);
118
119impl HttpBody {
120 pub fn is_empty(&self) -> bool {
122 self.0.is_null()
123 }
124
125 pub fn to_string_body(&self) -> Option<String> {
127 match &self.0 {
128 Value::Null => None,
129 Value::String(s) if s.is_empty() => None,
130 Value::String(s) => Some(s.clone()),
131 other => Some(other.to_string()),
132 }
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138#[serde(untagged)]
139pub enum HttpResponseBody {
140 #[serde(with = "base64_string")]
142 Binary(Vec<u8>),
143 Text(String),
145 Json(Value),
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, VariantNames)]
151#[serde(rename_all = "lowercase")]
152#[strum(serialize_all = "lowercase")]
153pub enum BodyType {
154 Json,
156 Text,
158 Binary,
160 Multipart,
162}
163
164impl EnumVariants for BodyType {
165 fn variant_names() -> &'static [&'static str] {
166 Self::VARIANTS
167 }
168}
169
170impl Default for BodyType {
171 fn default() -> Self {
172 Self::Json
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct MultipartPart {
179 pub name: String,
181
182 #[serde(flatten)]
184 pub content: MultipartContent,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
188#[serde(untagged)]
189pub enum MultipartContent {
190 Text { value: String },
192
193 File {
195 content: String,
197 #[serde(skip_serializing_if = "Option::is_none")]
199 filename: Option<String>,
200 #[serde(skip_serializing_if = "Option::is_none")]
202 #[serde(rename = "contentType")]
203 content_type: Option<String>,
204 },
205}
206
207#[derive(Debug, Serialize, Deserialize, CapabilityInput)]
209#[capability_input(display_name = "HTTP Request Input")]
210pub struct HttpRequestInput {
211 #[field(
213 display_name = "Method",
214 description = "HTTP verb for the request",
215 example = "GET",
216 default = "GET",
217 enum_type = "HttpMethod"
218 )]
219 #[serde(default)]
220 pub method: HttpMethod,
221
222 #[field(
224 display_name = "URL",
225 description = "Full URL to send the request to",
226 example = "https://api.example.com/v1/users"
227 )]
228 pub url: String,
229
230 #[field(
232 display_name = "Headers",
233 description = "Custom HTTP headers",
234 example = r#"{"Authorization": "Bearer token123"}"#,
235 default = "{}"
236 )]
237 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
238 pub headers: HashMap<String, String>,
239
240 #[field(
242 display_name = "Query Parameters",
243 description = "URL query parameters",
244 example = r#"{"page": "1", "limit": "100"}"#,
245 default = "{}"
246 )]
247 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
248 pub query_parameters: HashMap<String, String>,
249
250 #[field(
252 display_name = "Body",
253 description = "Request payload",
254 example = r#"{"name": "John Doe", "email": "john@example.com"}"#,
255 default = "null"
256 )]
257 #[serde(default, skip_serializing_if = "HttpBody::is_empty")]
258 pub body: HttpBody,
259
260 #[field(
262 display_name = "Body Type",
263 description = "How to encode the request body",
264 example = "json",
265 default = "json",
266 enum_type = "BodyType"
267 )]
268 #[serde(default)]
269 pub body_type: BodyType,
270
271 #[field(
273 display_name = "Multipart Parts",
274 description = "Form fields and files to include in multipart requests",
275 default = "[]"
276 )]
277 #[serde(default, skip_serializing_if = "Vec::is_empty")]
278 pub multipart: Vec<MultipartPart>,
279
280 #[field(
282 display_name = "Response Type",
283 description = "Expected response format",
284 example = "json",
285 default = "json",
286 enum_type = "ResponseType"
287 )]
288 #[serde(default)]
289 pub response_type: ResponseType,
290
291 #[field(
293 display_name = "Timeout (ms)",
294 description = "Maximum time to wait for response",
295 example = "5000",
296 default = "30000"
297 )]
298 #[serde(default = "default_timeout")]
299 pub timeout_ms: u64,
300
301 #[field(
303 display_name = "Fail on Error",
304 description = "If true (default), non-2xx responses will fail the step. If false, non-2xx responses are returned normally.",
305 example = "true",
306 default = "true"
307 )]
308 #[serde(default = "default_fail_on_error")]
309 pub fail_on_error: bool,
310
311 #[serde(skip_serializing_if = "Option::is_none")]
313 #[field(skip)]
314 pub _connection: Option<crate::connections::RawConnection>,
315}
316
317impl Default for HttpRequestInput {
318 fn default() -> Self {
319 HttpRequestInput {
320 method: HttpMethod::default(),
321 url: String::new(),
322 headers: HashMap::new(),
323 query_parameters: HashMap::new(),
324 body: HttpBody(Value::Null),
325 response_type: ResponseType::default(),
326 timeout_ms: default_timeout(),
327 body_type: BodyType::default(),
328 multipart: Vec::new(),
329 fail_on_error: default_fail_on_error(),
330 _connection: None,
331 }
332 }
333}
334
335fn default_timeout() -> u64 {
336 30000
337}
338
339fn default_fail_on_error() -> bool {
340 true
341}
342
343#[derive(Debug, Serialize, Deserialize)]
345#[allow(dead_code)]
346struct HttpResponseMetadata {
347 pub status_code: u16,
349
350 pub headers: HashMap<String, String>,
352
353 pub body_length: usize,
355
356 pub response_type: String,
358
359 pub success: bool,
361}
362
363#[derive(Debug, Serialize, Deserialize, CapabilityOutput)]
365#[capability_output(
366 display_name = "HTTP Response",
367 description = "Response from an HTTP request"
368)]
369pub struct HttpResponse {
370 #[field(
371 display_name = "Status Code",
372 description = "HTTP status code (e.g., 200, 404, 500)",
373 example = "200"
374 )]
375 pub status_code: u16,
376
377 #[field(
378 display_name = "Headers",
379 description = "Response headers as key-value pairs"
380 )]
381 pub headers: HashMap<String, String>,
382
383 #[field(
384 display_name = "Body",
385 description = "Response body (JSON object, text string, or base64-encoded binary depending on response_type)"
386 )]
387 pub body: HttpResponseBody,
388
389 #[field(
390 display_name = "Success",
391 description = "True if the status code is in the 2xx range",
392 example = "true"
393 )]
394 pub success: bool,
395}
396
397pub use crate::extractors::HttpConnectionConfig;
399
400pub fn extract_connection_config(
402 raw: &crate::connections::RawConnection,
403) -> Result<HttpConnectionConfig, String> {
404 crate::extractors::extract_http_config(
405 &raw.integration_id,
406 &raw.parameters,
407 raw.rate_limit_config.clone(),
408 )
409}
410
411#[capability(
417 module = "http",
418 display_name = "HTTP Request",
419 description = "Execute an HTTP request with the specified method, URL, headers, and body",
420 side_effects = true
421)]
422pub async fn http_request(input: HttpRequestInput) -> Result<HttpResponse, String> {
423 let mut headers = input.headers.clone();
425 let mut query_parameters = input.query_parameters.clone();
426 let mut url = input.url.clone();
427
428 if let Some(ref raw) = input._connection {
430 let config = extract_connection_config(raw)?;
431
432 if !url.starts_with("http://") && !url.starts_with("https://") {
434 url = format!("{}{}", config.url_prefix, url);
435 }
436
437 for (k, v) in config.headers {
439 headers.entry(k).or_insert(v);
440 }
441
442 for (k, v) in config.query_parameters {
444 query_parameters.entry(k).or_insert(v);
445 }
446
447 }
449
450 if !query_parameters.is_empty() {
452 let query_string: String = query_parameters
453 .iter()
454 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
455 .collect::<Vec<_>>()
456 .join("&");
457
458 if url.contains('?') {
459 url = format!("{}&{}", url, query_string);
460 } else {
461 url = format!("{}?{}", url, query_string);
462 }
463 }
464
465 let client = reqwest::Client::builder()
467 .timeout(std::time::Duration::from_millis(input.timeout_ms))
468 .build()
469 .map_err(|e| format!("Failed to create HTTP client: {}", e))?;
470
471 let method = match input.method {
473 HttpMethod::Get => reqwest::Method::GET,
474 HttpMethod::Post => reqwest::Method::POST,
475 HttpMethod::Put => reqwest::Method::PUT,
476 HttpMethod::Delete => reqwest::Method::DELETE,
477 HttpMethod::Patch => reqwest::Method::PATCH,
478 HttpMethod::Head => reqwest::Method::HEAD,
479 HttpMethod::Options => reqwest::Method::OPTIONS,
480 };
481
482 let mut request = client.request(method, &url);
483
484 for (key, value) in &headers {
486 request = request.header(key, value);
487 }
488
489 request = match input.method {
491 HttpMethod::Get | HttpMethod::Head | HttpMethod::Options | HttpMethod::Delete => request,
492 HttpMethod::Post | HttpMethod::Put | HttpMethod::Patch => {
493 if let Some(body_str) = input.body.to_string_body() {
494 if !headers.contains_key("Content-Type") && !headers.contains_key("content-type") {
496 request = request.header("Content-Type", "application/json");
497 }
498 request.body(body_str)
499 } else {
500 request
501 }
502 }
503 };
504
505 let response = request
507 .send()
508 .await
509 .map_err(|e| format!("HTTP request to {} failed: {}", input.url, e))?;
510
511 let status_code = response.status().as_u16();
512 let success = response.status().is_success();
513
514 let mut response_headers = HashMap::new();
516 for (name, value) in response.headers() {
517 if let Ok(v) = value.to_str() {
518 response_headers.insert(name.to_string(), v.to_string());
519 }
520 }
521
522 if !success && input.fail_on_error {
524 let body_text = response.text().await.unwrap_or_else(|_| String::new());
525 return Err(format!(
526 "HTTP request failed with status {}: {}",
527 status_code, body_text
528 ));
529 }
530
531 let body = match input.response_type {
533 ResponseType::Json => {
534 let text = response
535 .text()
536 .await
537 .map_err(|e| format!("Failed to read response body: {}", e))?;
538 match serde_json::from_str(&text) {
539 Ok(json_value) => HttpResponseBody::Json(json_value),
540 Err(_) => HttpResponseBody::Text(text),
541 }
542 }
543 ResponseType::Text => {
544 let text = response
545 .text()
546 .await
547 .map_err(|e| format!("Failed to read response body: {}", e))?;
548 HttpResponseBody::Text(text)
549 }
550 ResponseType::Binary => {
551 let bytes = response
552 .bytes()
553 .await
554 .map_err(|e| format!("Failed to read response body: {}", e))?;
555 HttpResponseBody::Binary(bytes.to_vec())
556 }
557 };
558
559 Ok(HttpResponse {
560 status_code,
561 headers: response_headers,
562 body,
563 success,
564 })
565}
566
567mod urlencoding {
569 pub fn encode(s: &str) -> String {
570 let mut result = String::new();
571 for c in s.chars() {
572 match c {
573 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '~' => result.push(c),
574 _ => {
575 for byte in c.to_string().as_bytes() {
576 result.push_str(&format!("%{:02X}", byte));
577 }
578 }
579 }
580 }
581 result
582 }
583}
584
585mod base64_string {
586 use base64::{Engine as _, engine::general_purpose};
587 use serde::{Deserialize, Deserializer, Serializer};
588
589 pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
590 where
591 S: Serializer,
592 {
593 let encoded = general_purpose::STANDARD.encode(bytes);
594 serializer.serialize_str(&encoded)
595 }
596
597 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
598 where
599 D: Deserializer<'de>,
600 {
601 let encoded = String::deserialize(deserializer)?;
602 general_purpose::STANDARD
603 .decode(encoded.as_bytes())
604 .map_err(serde::de::Error::custom)
605 }
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611 use wiremock::matchers::{body_string, header, method, path, query_param};
612 use wiremock::{Mock, MockServer, ResponseTemplate};
613
614 #[tokio::test]
615 async fn test_get_request_json_response() {
616 let mock_server = MockServer::start().await;
617
618 Mock::given(method("GET"))
619 .and(path("/users"))
620 .respond_with(
621 ResponseTemplate::new(200)
622 .set_body_json(serde_json::json!({"id": 1, "name": "John"})),
623 )
624 .mount(&mock_server)
625 .await;
626
627 let input = HttpRequestInput {
628 method: HttpMethod::Get,
629 url: format!("{}/users", mock_server.uri()),
630 response_type: ResponseType::Json,
631 ..Default::default()
632 };
633
634 let result = http_request(input).await;
635 assert!(result.is_ok());
636
637 let response = result.unwrap();
638 assert_eq!(response.status_code, 200);
639 assert!(response.success);
640 assert!(matches!(response.body, HttpResponseBody::Json(_)));
641
642 if let HttpResponseBody::Json(json) = response.body {
643 assert_eq!(json["id"], 1);
644 assert_eq!(json["name"], "John");
645 }
646 }
647
648 #[tokio::test]
649 async fn test_post_request_with_json_body() {
650 let mock_server = MockServer::start().await;
651
652 Mock::given(method("POST"))
653 .and(path("/users"))
654 .and(header("Content-Type", "application/json"))
655 .and(body_string(r#"{"name":"Jane"}"#))
656 .respond_with(
657 ResponseTemplate::new(201)
658 .set_body_json(serde_json::json!({"id": 2, "name": "Jane"})),
659 )
660 .mount(&mock_server)
661 .await;
662
663 let input = HttpRequestInput {
664 method: HttpMethod::Post,
665 url: format!("{}/users", mock_server.uri()),
666 body: HttpBody(serde_json::json!({"name": "Jane"})),
667 response_type: ResponseType::Json,
668 ..Default::default()
669 };
670
671 let result = http_request(input).await;
672 assert!(result.is_ok());
673
674 let response = result.unwrap();
675 assert_eq!(response.status_code, 201);
676 assert!(response.success);
677 }
678
679 #[tokio::test]
680 async fn test_get_request_with_query_parameters() {
681 let mock_server = MockServer::start().await;
682
683 Mock::given(method("GET"))
684 .and(path("/search"))
685 .and(query_param("q", "rust"))
686 .and(query_param("page", "1"))
687 .respond_with(
688 ResponseTemplate::new(200).set_body_json(serde_json::json!({"results": []})),
689 )
690 .mount(&mock_server)
691 .await;
692
693 let mut query_params = HashMap::new();
694 query_params.insert("q".to_string(), "rust".to_string());
695 query_params.insert("page".to_string(), "1".to_string());
696
697 let input = HttpRequestInput {
698 method: HttpMethod::Get,
699 url: format!("{}/search", mock_server.uri()),
700 query_parameters: query_params,
701 response_type: ResponseType::Json,
702 ..Default::default()
703 };
704
705 let result = http_request(input).await;
706 assert!(result.is_ok());
707 assert_eq!(result.unwrap().status_code, 200);
708 }
709
710 #[tokio::test]
711 async fn test_get_request_with_custom_headers() {
712 let mock_server = MockServer::start().await;
713
714 Mock::given(method("GET"))
715 .and(path("/protected"))
716 .and(header("Authorization", "Bearer token123"))
717 .and(header("X-Custom-Header", "custom-value"))
718 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"ok": true})))
719 .mount(&mock_server)
720 .await;
721
722 let mut headers = HashMap::new();
723 headers.insert("Authorization".to_string(), "Bearer token123".to_string());
724 headers.insert("X-Custom-Header".to_string(), "custom-value".to_string());
725
726 let input = HttpRequestInput {
727 method: HttpMethod::Get,
728 url: format!("{}/protected", mock_server.uri()),
729 headers,
730 response_type: ResponseType::Json,
731 ..Default::default()
732 };
733
734 let result = http_request(input).await;
735 assert!(result.is_ok());
736 assert_eq!(result.unwrap().status_code, 200);
737 }
738
739 #[tokio::test]
740 async fn test_text_response_type() {
741 let mock_server = MockServer::start().await;
742
743 Mock::given(method("GET"))
744 .and(path("/text"))
745 .respond_with(ResponseTemplate::new(200).set_body_string("Hello, World!"))
746 .mount(&mock_server)
747 .await;
748
749 let input = HttpRequestInput {
750 method: HttpMethod::Get,
751 url: format!("{}/text", mock_server.uri()),
752 response_type: ResponseType::Text,
753 ..Default::default()
754 };
755
756 let result = http_request(input).await;
757 assert!(result.is_ok());
758
759 let response = result.unwrap();
760 assert!(matches!(response.body, HttpResponseBody::Text(_)));
761
762 if let HttpResponseBody::Text(text) = response.body {
763 assert_eq!(text, "Hello, World!");
764 }
765 }
766
767 #[tokio::test]
768 async fn test_binary_response_type() {
769 let mock_server = MockServer::start().await;
770
771 let binary_data = vec![0x89, 0x50, 0x4E, 0x47]; Mock::given(method("GET"))
774 .and(path("/image"))
775 .respond_with(ResponseTemplate::new(200).set_body_bytes(binary_data.clone()))
776 .mount(&mock_server)
777 .await;
778
779 let input = HttpRequestInput {
780 method: HttpMethod::Get,
781 url: format!("{}/image", mock_server.uri()),
782 response_type: ResponseType::Binary,
783 ..Default::default()
784 };
785
786 let result = http_request(input).await;
787 assert!(result.is_ok());
788
789 let response = result.unwrap();
790 assert!(matches!(response.body, HttpResponseBody::Binary(_)));
791
792 if let HttpResponseBody::Binary(bytes) = response.body {
793 assert_eq!(bytes, binary_data);
794 }
795 }
796
797 #[tokio::test]
798 async fn test_put_request() {
799 let mock_server = MockServer::start().await;
800
801 Mock::given(method("PUT"))
802 .and(path("/users/1"))
803 .respond_with(
804 ResponseTemplate::new(200).set_body_json(serde_json::json!({"updated": true})),
805 )
806 .mount(&mock_server)
807 .await;
808
809 let input = HttpRequestInput {
810 method: HttpMethod::Put,
811 url: format!("{}/users/1", mock_server.uri()),
812 body: HttpBody(serde_json::json!({"name": "Updated"})),
813 ..Default::default()
814 };
815
816 let result = http_request(input).await;
817 assert!(result.is_ok());
818 assert_eq!(result.unwrap().status_code, 200);
819 }
820
821 #[tokio::test]
822 async fn test_delete_request() {
823 let mock_server = MockServer::start().await;
824
825 Mock::given(method("DELETE"))
826 .and(path("/users/1"))
827 .respond_with(ResponseTemplate::new(204))
828 .mount(&mock_server)
829 .await;
830
831 let input = HttpRequestInput {
832 method: HttpMethod::Delete,
833 url: format!("{}/users/1", mock_server.uri()),
834 ..Default::default()
835 };
836
837 let result = http_request(input).await;
838 assert!(result.is_ok());
839 assert_eq!(result.unwrap().status_code, 204);
840 }
841
842 #[tokio::test]
843 async fn test_patch_request() {
844 let mock_server = MockServer::start().await;
845
846 Mock::given(method("PATCH"))
847 .and(path("/users/1"))
848 .respond_with(
849 ResponseTemplate::new(200).set_body_json(serde_json::json!({"patched": true})),
850 )
851 .mount(&mock_server)
852 .await;
853
854 let input = HttpRequestInput {
855 method: HttpMethod::Patch,
856 url: format!("{}/users/1", mock_server.uri()),
857 body: HttpBody(serde_json::json!({"status": "active"})),
858 ..Default::default()
859 };
860
861 let result = http_request(input).await;
862 assert!(result.is_ok());
863 assert_eq!(result.unwrap().status_code, 200);
864 }
865
866 #[tokio::test]
867 async fn test_error_response_with_fail_on_error_true() {
868 let mock_server = MockServer::start().await;
869
870 Mock::given(method("GET"))
871 .and(path("/not-found"))
872 .respond_with(
873 ResponseTemplate::new(404).set_body_json(serde_json::json!({"error": "Not found"})),
874 )
875 .mount(&mock_server)
876 .await;
877
878 let input = HttpRequestInput {
879 method: HttpMethod::Get,
880 url: format!("{}/not-found", mock_server.uri()),
881 fail_on_error: true,
882 ..Default::default()
883 };
884
885 let result = http_request(input).await;
886 assert!(result.is_err());
887 assert!(result.unwrap_err().contains("404"));
888 }
889
890 #[tokio::test]
891 async fn test_error_response_with_fail_on_error_false() {
892 let mock_server = MockServer::start().await;
893
894 Mock::given(method("GET"))
895 .and(path("/not-found"))
896 .respond_with(
897 ResponseTemplate::new(404).set_body_json(serde_json::json!({"error": "Not found"})),
898 )
899 .mount(&mock_server)
900 .await;
901
902 let input = HttpRequestInput {
903 method: HttpMethod::Get,
904 url: format!("{}/not-found", mock_server.uri()),
905 fail_on_error: false,
906 ..Default::default()
907 };
908
909 let result = http_request(input).await;
910 assert!(result.is_ok());
911
912 let response = result.unwrap();
913 assert_eq!(response.status_code, 404);
914 assert!(!response.success);
915 }
916
917 #[tokio::test]
918 async fn test_server_error_with_fail_on_error_false() {
919 let mock_server = MockServer::start().await;
920
921 Mock::given(method("GET"))
922 .and(path("/error"))
923 .respond_with(
924 ResponseTemplate::new(500)
925 .set_body_json(serde_json::json!({"error": "Internal server error"})),
926 )
927 .mount(&mock_server)
928 .await;
929
930 let input = HttpRequestInput {
931 method: HttpMethod::Get,
932 url: format!("{}/error", mock_server.uri()),
933 fail_on_error: false,
934 ..Default::default()
935 };
936
937 let result = http_request(input).await;
938 assert!(result.is_ok());
939
940 let response = result.unwrap();
941 assert_eq!(response.status_code, 500);
942 assert!(!response.success);
943 }
944
945 #[tokio::test]
946 async fn test_response_headers_captured() {
947 let mock_server = MockServer::start().await;
948
949 Mock::given(method("GET"))
950 .and(path("/headers"))
951 .respond_with(
952 ResponseTemplate::new(200)
953 .insert_header("X-Custom-Response", "custom-value")
954 .insert_header("X-Request-Id", "12345")
955 .set_body_json(serde_json::json!({})),
956 )
957 .mount(&mock_server)
958 .await;
959
960 let input = HttpRequestInput {
961 method: HttpMethod::Get,
962 url: format!("{}/headers", mock_server.uri()),
963 ..Default::default()
964 };
965
966 let result = http_request(input).await;
967 assert!(result.is_ok());
968
969 let response = result.unwrap();
970 assert_eq!(
971 response.headers.get("x-custom-response"),
972 Some(&"custom-value".to_string())
973 );
974 assert_eq!(
975 response.headers.get("x-request-id"),
976 Some(&"12345".to_string())
977 );
978 }
979
980 #[tokio::test]
981 async fn test_head_request() {
982 let mock_server = MockServer::start().await;
983
984 Mock::given(method("HEAD"))
985 .and(path("/resource"))
986 .respond_with(ResponseTemplate::new(200).insert_header("Content-Length", "1024"))
987 .mount(&mock_server)
988 .await;
989
990 let input = HttpRequestInput {
991 method: HttpMethod::Head,
992 url: format!("{}/resource", mock_server.uri()),
993 ..Default::default()
994 };
995
996 let result = http_request(input).await;
997 assert!(result.is_ok());
998
999 let response = result.unwrap();
1000 assert_eq!(response.status_code, 200);
1001 }
1002
1003 #[tokio::test]
1004 async fn test_options_request() {
1005 let mock_server = MockServer::start().await;
1006
1007 Mock::given(method("OPTIONS"))
1008 .and(path("/api"))
1009 .respond_with(
1010 ResponseTemplate::new(200).insert_header("Allow", "GET, POST, PUT, DELETE"),
1011 )
1012 .mount(&mock_server)
1013 .await;
1014
1015 let input = HttpRequestInput {
1016 method: HttpMethod::Options,
1017 url: format!("{}/api", mock_server.uri()),
1018 ..Default::default()
1019 };
1020
1021 let result = http_request(input).await;
1022 assert!(result.is_ok());
1023
1024 let response = result.unwrap();
1025 assert_eq!(response.status_code, 200);
1026 assert!(response.headers.get("allow").is_some());
1027 }
1028
1029 #[tokio::test]
1030 async fn test_json_response_fallback_to_text() {
1031 let mock_server = MockServer::start().await;
1032
1033 Mock::given(method("GET"))
1035 .and(path("/invalid-json"))
1036 .respond_with(ResponseTemplate::new(200).set_body_string("not valid json"))
1037 .mount(&mock_server)
1038 .await;
1039
1040 let input = HttpRequestInput {
1041 method: HttpMethod::Get,
1042 url: format!("{}/invalid-json", mock_server.uri()),
1043 response_type: ResponseType::Json,
1044 ..Default::default()
1045 };
1046
1047 let result = http_request(input).await;
1048 assert!(result.is_ok());
1049
1050 let response = result.unwrap();
1051 assert!(matches!(response.body, HttpResponseBody::Text(_)));
1053
1054 if let HttpResponseBody::Text(text) = response.body {
1055 assert_eq!(text, "not valid json");
1056 }
1057 }
1058}