1use base64::prelude::*;
2use reqwest::header::{self, HeaderMap, HeaderValue};
3use reqwest::{Client, Method, RequestBuilder, StatusCode};
4use serde_json::Value;
5use std::collections::HashMap;
6use std::time::Duration;
7use tracing::{debug, error, info, info_span};
8use url::Url;
9
10use crate::error::{
11 Error, NetworkErrorCategory, ToolCallError, ToolCallExecutionError, ToolCallValidationError,
12};
13use crate::tool::ToolMetadata;
14use crate::tool_generator::{ExtractedParameters, QueryParameter, ToolGenerator};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct DataUriContent {
19 pub mime_type: String,
21 pub bytes: Vec<u8>,
23}
24
25pub fn parse_data_uri(value: &str, field_name: &str) -> Result<DataUriContent, Error> {
56 let format_error = || {
57 Error::Validation(format!(
58 "Invalid data URI format for field '{}': expected 'data:<mime>;base64,<content>'",
59 field_name
60 ))
61 };
62
63 let remainder = value.strip_prefix("data:").ok_or_else(format_error)?;
65
66 let base64_marker = ";base64,";
69 let marker_pos = remainder.find(base64_marker).ok_or_else(|| {
70 if let Some(semicolon_pos) = remainder.find(';')
72 && let Some(comma_pos) = remainder[semicolon_pos..].find(',')
73 {
74 let encoding = &remainder[semicolon_pos + 1..semicolon_pos + comma_pos];
75 if !encoding.is_empty() && encoding != "base64" {
76 return Error::Validation(format!(
77 "Unsupported encoding '{}' for field '{}': only base64 is supported",
78 encoding, field_name
79 ));
80 }
81 }
82 format_error()
83 })?;
84
85 let mime_type = &remainder[..marker_pos];
86 let content = &remainder[marker_pos + base64_marker.len()..];
87
88 if mime_type.is_empty() {
90 return Err(Error::Validation(format!(
91 "Invalid data URI format for field '{}': MIME type cannot be empty",
92 field_name
93 )));
94 }
95
96 let bytes = BASE64_STANDARD.decode(content).map_err(|e| {
98 Error::Validation(format!(
99 "Invalid base64 content for field '{}': {}",
100 field_name, e
101 ))
102 })?;
103
104 Ok(DataUriContent {
105 mime_type: mime_type.to_string(),
106 bytes,
107 })
108}
109
110const DEFAULT_TIMEOUT_SECONDS: u64 = 30;
113
114#[derive(Clone)]
116pub struct HttpClient {
117 client: Client,
118 base_url: Option<Url>,
119 default_headers: HeaderMap,
120 timeout_seconds: u64,
121}
122
123impl HttpClient {
124 fn create_user_agent() -> String {
126 format!("rmcp-openapi-server/{}", env!("CARGO_PKG_VERSION"))
127 }
128
129 fn build_reqwest_client(timeout_seconds: u64, insecure: bool) -> Client {
136 let user_agent = Self::create_user_agent();
137 let mut builder = Client::builder()
138 .user_agent(&user_agent)
139 .timeout(Duration::from_secs(timeout_seconds));
140
141 if insecure {
142 builder = builder
143 .danger_accept_invalid_certs(true)
144 .danger_accept_invalid_hostnames(true);
145 }
146
147 builder.build().expect("Failed to create HTTP client")
148 }
149
150 #[must_use]
156 pub fn new() -> Self {
157 Self {
158 client: Self::build_reqwest_client(DEFAULT_TIMEOUT_SECONDS, false),
159 base_url: None,
160 default_headers: HeaderMap::new(),
161 timeout_seconds: DEFAULT_TIMEOUT_SECONDS,
162 }
163 }
164
165 #[must_use]
171 pub fn with_timeout(timeout_seconds: u64) -> Self {
172 Self {
173 client: Self::build_reqwest_client(timeout_seconds, false),
174 base_url: None,
175 default_headers: HeaderMap::new(),
176 timeout_seconds,
177 }
178 }
179
180 #[must_use]
199 pub fn with_insecure(mut self, insecure: bool) -> Self {
200 if insecure {
201 self.client = Self::build_reqwest_client(self.timeout_seconds, true);
202 }
203 self
204 }
205
206 pub fn with_base_url(mut self, base_url: Url) -> Result<Self, Error> {
212 let mut base_url = base_url;
214 if !base_url.path().ends_with('/') {
215 base_url.set_path(&format!("{}/", base_url.path()));
216 }
217 self.base_url = Some(base_url);
218 Ok(self)
219 }
220
221 #[must_use]
223 pub fn with_default_headers(mut self, default_headers: HeaderMap) -> Self {
224 self.default_headers = default_headers;
225 self
226 }
227
228 #[must_use]
233 pub fn with_authorization(&self, auth_value: &str) -> Self {
234 let mut headers = self.default_headers.clone();
235 if let Ok(header_value) = HeaderValue::from_str(auth_value) {
236 headers.insert(header::AUTHORIZATION, header_value);
237 }
238
239 Self {
240 client: self.client.clone(),
241 base_url: self.base_url.clone(),
242 default_headers: headers,
243 timeout_seconds: self.timeout_seconds,
244 }
245 }
246
247 pub async fn execute_tool_call(
253 &self,
254 tool_metadata: &ToolMetadata,
255 arguments: &Value,
256 ) -> Result<HttpResponse, ToolCallError> {
257 let span = info_span!(
258 "http_request",
259 operation_id = %tool_metadata.name,
260 method = %tool_metadata.method,
261 path = %tool_metadata.path
262 );
263 let _enter = span.enter();
264
265 debug!(
266 "Executing tool call: {} {} with arguments: {}",
267 tool_metadata.method,
268 tool_metadata.path,
269 serde_json::to_string_pretty(arguments).unwrap_or_else(|_| "invalid json".to_string())
270 );
271
272 let extracted_params = ToolGenerator::extract_parameters(tool_metadata, arguments)?;
274
275 debug!(
276 "Extracted parameters: path={:?}, query={:?}, headers={:?}, cookies={:?}",
277 extracted_params.path,
278 extracted_params.query,
279 extracted_params.headers,
280 extracted_params.cookies
281 );
282
283 let mut url = self
285 .build_url(tool_metadata, &extracted_params)
286 .map_err(|e| {
287 ToolCallError::Validation(ToolCallValidationError::RequestConstructionError {
288 reason: e.to_string(),
289 })
290 })?;
291
292 if !extracted_params.query.is_empty() {
294 Self::add_query_parameters(&mut url, &extracted_params.query);
295 }
296
297 info!("Final URL: {}", url);
298
299 let mut request = self
301 .create_request(&tool_metadata.method, &url)
302 .map_err(|e| {
303 ToolCallError::Validation(ToolCallValidationError::RequestConstructionError {
304 reason: e.to_string(),
305 })
306 })?;
307
308 if !self.default_headers.is_empty() {
310 request = Self::add_headers_from_map(request, &self.default_headers);
312 }
313
314 if !extracted_params.headers.is_empty() {
316 request = Self::add_headers(request, &extracted_params.headers);
317 }
318
319 if !extracted_params.cookies.is_empty() {
321 request = Self::add_cookies(request, &extracted_params.cookies);
322 }
323
324 if !extracted_params.body.is_empty() {
326 request =
327 Self::add_request_body(request, &extracted_params.body, &extracted_params.config)
328 .map_err(|e| {
329 ToolCallError::Execution(ToolCallExecutionError::ResponseParsingError {
330 reason: format!("Failed to serialize request body: {e}"),
331 raw_response: None,
332 })
333 })?;
334 }
335
336 if extracted_params.config.timeout_seconds != 30 {
338 request = request.timeout(Duration::from_secs(u64::from(
339 extracted_params.config.timeout_seconds,
340 )));
341 }
342
343 let request_body_string = if extracted_params.body.is_empty() {
345 String::new()
346 } else if extracted_params.body.len() == 1
347 && extracted_params.body.contains_key("request_body")
348 {
349 serde_json::to_string(&extracted_params.body["request_body"]).unwrap_or_default()
350 } else {
351 let body_object = Value::Object(
352 extracted_params
353 .body
354 .iter()
355 .map(|(k, v)| (k.clone(), v.clone()))
356 .collect(),
357 );
358 serde_json::to_string(&body_object).unwrap_or_default()
359 };
360
361 let final_url = url.to_string();
363
364 debug!("Sending HTTP request...");
366 let start_time = std::time::Instant::now();
367 let response = request.send().await.map_err(|e| {
368 error!(
369 operation_id = %tool_metadata.name,
370 method = %tool_metadata.method,
371 url = %final_url,
372 error = %e,
373 "HTTP request failed"
374 );
375
376 let (error_msg, category) = if e.is_timeout() {
378 (
379 format!(
380 "Request timeout after {} seconds while calling {} {}",
381 extracted_params.config.timeout_seconds,
382 tool_metadata.method.to_uppercase(),
383 final_url
384 ),
385 NetworkErrorCategory::Timeout,
386 )
387 } else if e.is_connect() {
388 (
389 format!(
390 "Connection failed to {final_url} - Error: {e}. Check if the server is running and the URL is correct."
391 ),
392 NetworkErrorCategory::Connect,
393 )
394 } else if e.is_request() {
395 (
396 format!(
397 "Request error while calling {} {} - Error: {}",
398 tool_metadata.method.to_uppercase(),
399 final_url,
400 e
401 ),
402 NetworkErrorCategory::Request,
403 )
404 } else if e.is_body() {
405 (
406 format!(
407 "Body error while calling {} {} - Error: {}",
408 tool_metadata.method.to_uppercase(),
409 final_url,
410 e
411 ),
412 NetworkErrorCategory::Body,
413 )
414 } else if e.is_decode() {
415 (
416 format!(
417 "Response decode error from {} {} - Error: {}",
418 tool_metadata.method.to_uppercase(),
419 final_url,
420 e
421 ),
422 NetworkErrorCategory::Decode,
423 )
424 } else {
425 (
426 format!(
427 "HTTP request failed: {} (URL: {}, Method: {})",
428 e,
429 final_url,
430 tool_metadata.method.to_uppercase()
431 ),
432 NetworkErrorCategory::Other,
433 )
434 };
435
436 ToolCallError::Execution(ToolCallExecutionError::NetworkError {
437 message: error_msg,
438 category,
439 })
440 })?;
441
442 let elapsed = start_time.elapsed();
443 info!(
444 operation_id = %tool_metadata.name,
445 method = %tool_metadata.method,
446 url = %final_url,
447 status = response.status().as_u16(),
448 elapsed_ms = elapsed.as_millis(),
449 "HTTP request completed"
450 );
451 debug!("Response received with status: {}", response.status());
452
453 self.process_response_with_request(
455 response,
456 &tool_metadata.method,
457 &final_url,
458 &request_body_string,
459 )
460 .await
461 .map_err(|e| {
462 ToolCallError::Execution(ToolCallExecutionError::HttpError {
463 status: 0,
464 message: e.to_string(),
465 details: None,
466 })
467 })
468 }
469
470 fn build_url(
472 &self,
473 tool_metadata: &ToolMetadata,
474 extracted_params: &ExtractedParameters,
475 ) -> Result<Url, Error> {
476 let mut path = tool_metadata.path.clone();
477
478 for (param_name, param_value) in &extracted_params.path {
480 let placeholder = format!("{{{param_name}}}");
481 let value_str = match param_value {
482 Value::String(s) => s.clone(),
483 Value::Number(n) => n.to_string(),
484 Value::Bool(b) => b.to_string(),
485 _ => param_value.to_string(),
486 };
487 path = path.replace(&placeholder, &value_str);
488 }
489
490 let mut path: &str = path.as_ref();
491
492 if let Some(base_url) = &self.base_url {
494 if path.starts_with('/') {
497 path = &path[1..];
498 }
499 base_url.join(path).map_err(|e| {
500 Error::Http(format!(
501 "Failed to join URL '{base_url}' with path '{path}': {e}"
502 ))
503 })
504 } else {
505 if path.starts_with("http") {
507 Url::parse(path).map_err(|e| Error::Http(format!("Invalid URL '{path}': {e}")))
508 } else {
509 Err(Error::Http(
510 "No base URL configured and path is not a complete URL".to_string(),
511 ))
512 }
513 }
514 }
515
516 fn create_request(&self, method: &str, url: &Url) -> Result<RequestBuilder, Error> {
518 let http_method = method.to_uppercase();
519 let method = match http_method.as_str() {
520 "GET" => Method::GET,
521 "POST" => Method::POST,
522 "PUT" => Method::PUT,
523 "DELETE" => Method::DELETE,
524 "PATCH" => Method::PATCH,
525 "HEAD" => Method::HEAD,
526 "OPTIONS" => Method::OPTIONS,
527 _ => {
528 return Err(Error::Http(format!(
529 "Unsupported HTTP method: {http_method}"
530 )));
531 }
532 };
533
534 Ok(self.client.request(method, url.clone()))
535 }
536
537 fn add_query_parameters(url: &mut Url, query_params: &HashMap<String, QueryParameter>) {
539 fn scalar_to_string(value: &Value) -> String {
542 match value {
543 Value::String(s) => s.clone(),
544 Value::Number(n) => n.to_string(),
545 Value::Bool(b) => b.to_string(),
546 other => other.to_string(),
547 }
548 }
549
550 {
551 let mut query_pairs = url.query_pairs_mut();
552 for (key, query_param) in query_params {
553 match &query_param.value {
554 Value::Array(arr) => {
555 if query_param.explode {
556 for item in arr {
558 query_pairs.append_pair(key, &scalar_to_string(item));
559 }
560 } else {
561 let comma_separated = arr
563 .iter()
564 .map(scalar_to_string)
565 .collect::<Vec<_>>()
566 .join(",");
567 query_pairs.append_pair(key, &comma_separated);
568 }
569 }
570 Value::Object(map) => {
571 for (property, property_value) in map {
577 let nested_key = format!("{key}[{property}]");
578 let value_str = match property_value {
579 Value::Array(items) => items
580 .iter()
581 .map(scalar_to_string)
582 .collect::<Vec<_>>()
583 .join(","),
584 scalar => scalar_to_string(scalar),
585 };
586 query_pairs.append_pair(&nested_key, &value_str);
587 }
588 }
589 scalar => {
590 query_pairs.append_pair(key, &scalar_to_string(scalar));
591 }
592 }
593 }
594 }
595 }
596
597 fn add_headers_from_map(mut request: RequestBuilder, headers: &HeaderMap) -> RequestBuilder {
599 for (key, value) in headers {
600 request = request.header(key, value);
602 }
603 request
604 }
605
606 fn add_headers(
608 mut request: RequestBuilder,
609 headers: &HashMap<String, Value>,
610 ) -> RequestBuilder {
611 for (key, value) in headers {
612 let value_str = match value {
613 Value::String(s) => s.clone(),
614 Value::Number(n) => n.to_string(),
615 Value::Bool(b) => b.to_string(),
616 _ => value.to_string(),
617 };
618 request = request.header(key, value_str);
619 }
620 request
621 }
622
623 fn add_cookies(
625 mut request: RequestBuilder,
626 cookies: &HashMap<String, Value>,
627 ) -> RequestBuilder {
628 if !cookies.is_empty() {
629 let cookie_header = cookies
630 .iter()
631 .map(|(key, value)| {
632 let value_str = match value {
633 Value::String(s) => s.clone(),
634 Value::Number(n) => n.to_string(),
635 Value::Bool(b) => b.to_string(),
636 _ => value.to_string(),
637 };
638 format!("{key}={value_str}")
639 })
640 .collect::<Vec<_>>()
641 .join("; ");
642
643 request = request.header(header::COOKIE, cookie_header);
644 }
645 request
646 }
647
648 fn add_request_body(
650 mut request: RequestBuilder,
651 body: &HashMap<String, Value>,
652 config: &crate::tool_generator::RequestConfig,
653 ) -> Result<RequestBuilder, Error> {
654 if body.is_empty() {
655 return Ok(request);
656 }
657
658 match config.content_type.as_str() {
660 s if s == mime::APPLICATION_JSON.as_ref() => {
661 request = request.header(header::CONTENT_TYPE, &config.content_type);
663
664 if body.len() == 1 && body.contains_key("request_body") {
666 let body_value = &body["request_body"];
668 let json_string = serde_json::to_string(body_value).map_err(|e| {
669 Error::Http(format!("Failed to serialize request body: {e}"))
670 })?;
671 request = request.body(json_string);
672 } else {
673 let body_object =
675 Value::Object(body.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
676 let json_string = serde_json::to_string(&body_object).map_err(|e| {
677 Error::Http(format!("Failed to serialize request body: {e}"))
678 })?;
679 request = request.body(json_string);
680 }
681 }
682 s if s == mime::APPLICATION_WWW_FORM_URLENCODED.as_ref() => {
683 request = request.header(header::CONTENT_TYPE, &config.content_type);
685
686 let form_data: Vec<(String, String)> = body
688 .iter()
689 .map(|(key, value)| {
690 let value_str = match value {
691 Value::String(s) => s.clone(),
692 Value::Number(n) => n.to_string(),
693 Value::Bool(b) => b.to_string(),
694 _ => value.to_string(),
695 };
696 (key.clone(), value_str)
697 })
698 .collect();
699 request = request.form(&form_data);
700 }
701 s if s == mime::MULTIPART_FORM_DATA.as_ref() => {
702 let mut form = reqwest::multipart::Form::new();
704
705 for (key, value) in body {
706 if let Some(obj) = value.as_object()
708 && let Some(content_value) = obj.get("content")
709 && let Some(content_str) = content_value.as_str()
710 && content_str.starts_with("data:")
711 {
712 let data_uri = parse_data_uri(content_str, key)?;
714
715 let filename = obj
717 .get("filename")
718 .and_then(|v| v.as_str())
719 .unwrap_or("file")
720 .to_string();
721
722 let part = reqwest::multipart::Part::bytes(data_uri.bytes)
724 .file_name(filename)
725 .mime_str(&data_uri.mime_type)
726 .map_err(|e| Error::Http(format!("Invalid MIME type: {e}")))?;
727
728 form = form.part(key.clone(), part);
729 continue;
730 }
731
732 let text_value = match value {
734 Value::String(s) => s.clone(),
735 Value::Number(n) => n.to_string(),
736 Value::Bool(b) => b.to_string(),
737 _ => value.to_string(),
738 };
739 form = form.text(key.clone(), text_value);
740 }
741
742 request = request.multipart(form);
743 }
744 _ => {
745 request = request.header(header::CONTENT_TYPE, &config.content_type);
747
748 let body_object =
750 Value::Object(body.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
751 let json_string = serde_json::to_string(&body_object)
752 .map_err(|e| Error::Http(format!("Failed to serialize request body: {e}")))?;
753 request = request.body(json_string);
754 }
755 }
756
757 Ok(request)
758 }
759
760 async fn process_response_with_request(
762 &self,
763 response: reqwest::Response,
764 method: &str,
765 url: &str,
766 request_body: &str,
767 ) -> Result<HttpResponse, Error> {
768 let status = response.status();
769
770 let content_type = response
772 .headers()
773 .get(header::CONTENT_TYPE)
774 .and_then(|v| v.to_str().ok())
775 .map(|s| s.to_string());
776
777 let is_binary_content = content_type
779 .as_ref()
780 .and_then(|ct| ct.parse::<mime::Mime>().ok())
781 .map(|mime_type| matches!(mime_type.type_(), mime::IMAGE | mime::AUDIO | mime::VIDEO))
782 .unwrap_or(false);
783
784 let headers = response
785 .headers()
786 .iter()
787 .map(|(name, value)| {
788 (
789 name.to_string(),
790 value.to_str().unwrap_or("<invalid>").to_string(),
791 )
792 })
793 .collect();
794
795 let (body, body_bytes) = if is_binary_content {
797 let bytes = response
799 .bytes()
800 .await
801 .map_err(|e| Error::Http(format!("Failed to read response body: {e}")))?;
802
803 let body_text = format!(
805 "[Binary content: {} bytes, Content-Type: {}]",
806 bytes.len(),
807 content_type.as_ref().unwrap_or(&"unknown".to_string())
808 );
809
810 (body_text, Some(bytes.to_vec()))
811 } else {
812 let text = response
814 .text()
815 .await
816 .map_err(|e| Error::Http(format!("Failed to read response body: {e}")))?;
817
818 (text, None)
819 };
820
821 let is_success = status.is_success();
822 let status_code = status.as_u16();
823 let status_text = status.canonical_reason().unwrap_or("Unknown").to_string();
824
825 let enhanced_status_text = match status {
827 StatusCode::BAD_REQUEST => {
828 format!("{status_text} - Bad Request: Check request parameters")
829 }
830 StatusCode::UNAUTHORIZED => {
831 format!("{status_text} - Unauthorized: Authentication required")
832 }
833 StatusCode::FORBIDDEN => format!("{status_text} - Forbidden: Access denied"),
834 StatusCode::NOT_FOUND => {
835 format!("{status_text} - Not Found: Endpoint or resource does not exist")
836 }
837 StatusCode::METHOD_NOT_ALLOWED => format!(
838 "{} - Method Not Allowed: {} method not supported",
839 status_text,
840 method.to_uppercase()
841 ),
842 StatusCode::UNPROCESSABLE_ENTITY => {
843 format!("{status_text} - Unprocessable Entity: Request validation failed")
844 }
845 StatusCode::TOO_MANY_REQUESTS => {
846 format!("{status_text} - Too Many Requests: Rate limit exceeded")
847 }
848 StatusCode::INTERNAL_SERVER_ERROR => {
849 format!("{status_text} - Internal Server Error: Server encountered an error")
850 }
851 StatusCode::BAD_GATEWAY => {
852 format!("{status_text} - Bad Gateway: Upstream server error")
853 }
854 StatusCode::SERVICE_UNAVAILABLE => {
855 format!("{status_text} - Service Unavailable: Server temporarily unavailable")
856 }
857 StatusCode::GATEWAY_TIMEOUT => {
858 format!("{status_text} - Gateway Timeout: Upstream server timeout")
859 }
860 _ => status_text,
861 };
862
863 Ok(HttpResponse {
864 status_code,
865 status_text: enhanced_status_text,
866 headers,
867 content_type,
868 body,
869 body_bytes,
870 is_success,
871 request_method: method.to_string(),
872 request_url: url.to_string(),
873 request_body: request_body.to_string(),
874 })
875 }
876}
877
878impl Default for HttpClient {
879 fn default() -> Self {
880 Self::new()
881 }
882}
883
884#[derive(Debug, Clone)]
886pub struct HttpResponse {
887 pub status_code: u16,
888 pub status_text: String,
889 pub headers: HashMap<String, String>,
890 pub content_type: Option<String>,
891 pub body: String,
892 pub body_bytes: Option<Vec<u8>>,
893 pub is_success: bool,
894 pub request_method: String,
895 pub request_url: String,
896 pub request_body: String,
897}
898
899impl HttpResponse {
900 pub fn json(&self) -> Result<Value, Error> {
906 serde_json::from_str(&self.body)
907 .map_err(|e| Error::Http(format!("Failed to parse response as JSON: {e}")))
908 }
909
910 #[must_use]
914 pub fn is_image(&self) -> bool {
915 self.content_type
916 .as_ref()
917 .and_then(|ct| ct.parse::<mime::Mime>().ok())
918 .map(|mime_type| mime_type.type_() == mime::IMAGE)
919 .unwrap_or(false)
920 }
921
922 #[must_use]
926 pub fn is_binary(&self) -> bool {
927 self.content_type
928 .as_ref()
929 .and_then(|ct| ct.parse::<mime::Mime>().ok())
930 .map(|mime_type| matches!(mime_type.type_(), mime::IMAGE | mime::AUDIO | mime::VIDEO))
931 .unwrap_or(false)
932 }
933
934 #[must_use]
936 pub fn to_mcp_content(&self) -> String {
937 let method = if self.request_method.is_empty() {
938 None
939 } else {
940 Some(self.request_method.as_str())
941 };
942 let url = if self.request_url.is_empty() {
943 None
944 } else {
945 Some(self.request_url.as_str())
946 };
947 let body = if self.request_body.is_empty() {
948 None
949 } else {
950 Some(self.request_body.as_str())
951 };
952 self.to_mcp_content_with_request(method, url, body)
953 }
954
955 pub fn to_mcp_content_with_request(
957 &self,
958 method: Option<&str>,
959 url: Option<&str>,
960 request_body: Option<&str>,
961 ) -> String {
962 let mut result = format!(
963 "HTTP {} {}\n\nStatus: {} {}\n",
964 if self.is_success { "✅" } else { "❌" },
965 if self.is_success { "Success" } else { "Error" },
966 self.status_code,
967 self.status_text
968 );
969
970 if let (Some(method), Some(url)) = (method, url) {
972 result.push_str("\nRequest: ");
973 result.push_str(&method.to_uppercase());
974 result.push(' ');
975 result.push_str(url);
976 result.push('\n');
977
978 if let Some(body) = request_body
979 && !body.is_empty()
980 && body != "{}"
981 {
982 result.push_str("\nRequest Body:\n");
983 if let Ok(parsed) = serde_json::from_str::<Value>(body) {
984 if let Ok(pretty) = serde_json::to_string_pretty(&parsed) {
985 result.push_str(&pretty);
986 } else {
987 result.push_str(body);
988 }
989 } else {
990 result.push_str(body);
991 }
992 result.push('\n');
993 }
994 }
995
996 if !self.headers.is_empty() {
998 result.push_str("\nHeaders:\n");
999 for (key, value) in &self.headers {
1000 if [
1002 header::CONTENT_TYPE.as_str(),
1003 header::CONTENT_LENGTH.as_str(),
1004 header::LOCATION.as_str(),
1005 header::SET_COOKIE.as_str(),
1006 ]
1007 .iter()
1008 .any(|&h| key.to_lowercase().contains(h))
1009 {
1010 result.push_str(" ");
1011 result.push_str(key);
1012 result.push_str(": ");
1013 result.push_str(value);
1014 result.push('\n');
1015 }
1016 }
1017 }
1018
1019 result.push_str("\nResponse Body:\n");
1021 if self.body.is_empty() {
1022 result.push_str("(empty)");
1023 } else if let Ok(json_value) = self.json() {
1024 match serde_json::to_string_pretty(&json_value) {
1026 Ok(pretty) => result.push_str(&pretty),
1027 Err(_) => result.push_str(&self.body),
1028 }
1029 } else {
1030 if self.body.len() > 2000 {
1032 result.push_str(&self.body[..2000]);
1033 result.push_str("\n... (");
1034 result.push_str(&(self.body.len() - 2000).to_string());
1035 result.push_str(" more characters)");
1036 } else {
1037 result.push_str(&self.body);
1038 }
1039 }
1040
1041 result
1042 }
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047 use super::*;
1048 use crate::tool_generator::ExtractedParameters;
1049 use serde_json::json;
1050 use std::collections::HashMap;
1051
1052 #[test]
1053 fn test_with_base_url_validation() {
1054 let url = Url::parse("https://api.example.com").unwrap();
1056 let client = HttpClient::new().with_base_url(url);
1057 assert!(client.is_ok());
1058
1059 let url = Url::parse("http://localhost:8080").unwrap();
1060 let client = HttpClient::new().with_base_url(url);
1061 assert!(client.is_ok());
1062
1063 assert!(Url::parse("not-a-url").is_err());
1065 assert!(Url::parse("").is_err());
1066
1067 let url = Url::parse("ftp://invalid-scheme.com").unwrap();
1069 let client = HttpClient::new().with_base_url(url);
1070 assert!(client.is_ok()); }
1072
1073 #[test]
1074 fn test_build_url_with_base_url() {
1075 let base_url = Url::parse("https://api.example.com").unwrap();
1076 let client = HttpClient::new().with_base_url(base_url).unwrap();
1077
1078 let tool_metadata = crate::ToolMetadata {
1079 name: "test".to_string(),
1080 title: None,
1081 description: Some("test".to_string()),
1082 parameters: json!({}),
1083 output_schema: None,
1084 method: "GET".to_string(),
1085 path: "/pets/{id}".to_string(),
1086 security: None,
1087 parameter_mappings: std::collections::HashMap::new(),
1088 };
1089
1090 let mut path_params = HashMap::new();
1091 path_params.insert("id".to_string(), json!(123));
1092
1093 let extracted_params = ExtractedParameters {
1094 path: path_params,
1095 query: HashMap::new(),
1096 headers: HashMap::new(),
1097 cookies: HashMap::new(),
1098 body: HashMap::new(),
1099 config: crate::tool_generator::RequestConfig::default(),
1100 };
1101
1102 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1103 assert_eq!(url.to_string(), "https://api.example.com/pets/123");
1104 }
1105
1106 #[test]
1107 fn test_build_url_with_base_url_containing_path() {
1108 let test_cases = vec![
1109 "https://api.example.com/api/v4",
1110 "https://api.example.com/api/v4/",
1111 ];
1112
1113 for base_url in test_cases {
1114 let base_url = Url::parse(base_url).unwrap();
1115 let client = HttpClient::new().with_base_url(base_url).unwrap();
1116
1117 let tool_metadata = crate::ToolMetadata {
1118 name: "test".to_string(),
1119 title: None,
1120 description: Some("test".to_string()),
1121 parameters: json!({}),
1122 output_schema: None,
1123 method: "GET".to_string(),
1124 path: "/pets/{id}".to_string(),
1125 security: None,
1126 parameter_mappings: std::collections::HashMap::new(),
1127 };
1128
1129 let mut path_params = HashMap::new();
1130 path_params.insert("id".to_string(), json!(123));
1131
1132 let extracted_params = ExtractedParameters {
1133 path: path_params,
1134 query: HashMap::new(),
1135 headers: HashMap::new(),
1136 cookies: HashMap::new(),
1137 body: HashMap::new(),
1138 config: crate::tool_generator::RequestConfig::default(),
1139 };
1140
1141 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1142 assert_eq!(url.to_string(), "https://api.example.com/api/v4/pets/123");
1143 }
1144 }
1145
1146 #[test]
1147 fn test_build_url_without_base_url() {
1148 let client = HttpClient::new();
1149
1150 let tool_metadata = crate::ToolMetadata {
1151 name: "test".to_string(),
1152 title: None,
1153 description: Some("test".to_string()),
1154 parameters: json!({}),
1155 output_schema: None,
1156 method: "GET".to_string(),
1157 path: "https://api.example.com/pets/123".to_string(),
1158 security: None,
1159 parameter_mappings: std::collections::HashMap::new(),
1160 };
1161
1162 let extracted_params = ExtractedParameters {
1163 path: HashMap::new(),
1164 query: HashMap::new(),
1165 headers: HashMap::new(),
1166 cookies: HashMap::new(),
1167 body: HashMap::new(),
1168 config: crate::tool_generator::RequestConfig::default(),
1169 };
1170
1171 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1172 assert_eq!(url.to_string(), "https://api.example.com/pets/123");
1173
1174 let tool_metadata_relative = crate::ToolMetadata {
1176 name: "test".to_string(),
1177 title: None,
1178 description: Some("test".to_string()),
1179 parameters: json!({}),
1180 output_schema: None,
1181 method: "GET".to_string(),
1182 path: "/pets/123".to_string(),
1183 security: None,
1184 parameter_mappings: std::collections::HashMap::new(),
1185 };
1186
1187 let result = client.build_url(&tool_metadata_relative, &extracted_params);
1188 assert!(result.is_err());
1189 assert!(
1190 result
1191 .unwrap_err()
1192 .to_string()
1193 .contains("No base URL configured")
1194 );
1195 }
1196
1197 #[test]
1198 fn test_query_parameter_encoding_integration() {
1199 let base_url = Url::parse("https://api.example.com").unwrap();
1200 let client = HttpClient::new().with_base_url(base_url).unwrap();
1201
1202 let tool_metadata = crate::ToolMetadata {
1203 name: "test".to_string(),
1204 title: None,
1205 description: Some("test".to_string()),
1206 parameters: json!({}),
1207 output_schema: None,
1208 method: "GET".to_string(),
1209 path: "/search".to_string(),
1210 security: None,
1211 parameter_mappings: std::collections::HashMap::new(),
1212 };
1213
1214 let mut query_params = HashMap::new();
1216 query_params.insert(
1217 "q".to_string(),
1218 QueryParameter::new(json!("hello world"), true),
1219 ); query_params.insert(
1221 "category".to_string(),
1222 QueryParameter::new(json!("pets&dogs"), true),
1223 ); query_params.insert(
1225 "special".to_string(),
1226 QueryParameter::new(json!("foo=bar"), true),
1227 ); query_params.insert(
1229 "unicode".to_string(),
1230 QueryParameter::new(json!("café"), true),
1231 ); query_params.insert(
1233 "percent".to_string(),
1234 QueryParameter::new(json!("100%"), true),
1235 ); let extracted_params = ExtractedParameters {
1238 path: HashMap::new(),
1239 query: query_params,
1240 headers: HashMap::new(),
1241 cookies: HashMap::new(),
1242 body: HashMap::new(),
1243 config: crate::tool_generator::RequestConfig::default(),
1244 };
1245
1246 let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1247 HttpClient::add_query_parameters(&mut url, &extracted_params.query);
1248
1249 let url_string = url.to_string();
1250
1251 assert!(url_string.contains("q=hello+world")); assert!(url_string.contains("category=pets%26dogs")); assert!(url_string.contains("special=foo%3Dbar")); assert!(url_string.contains("unicode=caf%C3%A9")); assert!(url_string.contains("percent=100%25")); }
1259
1260 #[test]
1261 fn test_array_query_parameters() {
1262 let base_url = Url::parse("https://api.example.com").unwrap();
1263 let client = HttpClient::new().with_base_url(base_url).unwrap();
1264
1265 let tool_metadata = crate::ToolMetadata {
1266 name: "test".to_string(),
1267 title: None,
1268 description: Some("test".to_string()),
1269 parameters: json!({}),
1270 output_schema: None,
1271 method: "GET".to_string(),
1272 path: "/search".to_string(),
1273 security: None,
1274 parameter_mappings: std::collections::HashMap::new(),
1275 };
1276
1277 let mut query_params = HashMap::new();
1278 query_params.insert(
1279 "status".to_string(),
1280 QueryParameter::new(json!(["available", "pending"]), true),
1281 );
1282 query_params.insert(
1283 "tags".to_string(),
1284 QueryParameter::new(json!(["red & blue", "fast=car"]), true),
1285 );
1286
1287 let extracted_params = ExtractedParameters {
1288 path: HashMap::new(),
1289 query: query_params,
1290 headers: HashMap::new(),
1291 cookies: HashMap::new(),
1292 body: HashMap::new(),
1293 config: crate::tool_generator::RequestConfig::default(),
1294 };
1295
1296 let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1297 HttpClient::add_query_parameters(&mut url, &extracted_params.query);
1298
1299 let url_string = url.to_string();
1300
1301 assert!(url_string.contains("status=available"));
1303 assert!(url_string.contains("status=pending"));
1304 assert!(url_string.contains("tags=red+%26+blue")); assert!(url_string.contains("tags=fast%3Dcar")); }
1307
1308 #[test]
1309 fn test_path_parameter_substitution() {
1310 let base_url = Url::parse("https://api.example.com").unwrap();
1311 let client = HttpClient::new().with_base_url(base_url).unwrap();
1312
1313 let tool_metadata = crate::ToolMetadata {
1314 name: "test".to_string(),
1315 title: None,
1316 description: Some("test".to_string()),
1317 parameters: json!({}),
1318 output_schema: None,
1319 method: "GET".to_string(),
1320 path: "/users/{userId}/pets/{petId}".to_string(),
1321 security: None,
1322 parameter_mappings: std::collections::HashMap::new(),
1323 };
1324
1325 let mut path_params = HashMap::new();
1326 path_params.insert("userId".to_string(), json!(42));
1327 path_params.insert("petId".to_string(), json!("special-pet-123"));
1328
1329 let extracted_params = ExtractedParameters {
1330 path: path_params,
1331 query: HashMap::new(),
1332 headers: HashMap::new(),
1333 cookies: HashMap::new(),
1334 body: HashMap::new(),
1335 config: crate::tool_generator::RequestConfig::default(),
1336 };
1337
1338 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1339 assert_eq!(
1340 url.to_string(),
1341 "https://api.example.com/users/42/pets/special-pet-123"
1342 );
1343 }
1344
1345 #[test]
1346 fn test_url_join_edge_cases() {
1347 let base_url1 = Url::parse("https://api.example.com/").unwrap();
1349 let client1 = HttpClient::new().with_base_url(base_url1).unwrap();
1350
1351 let base_url2 = Url::parse("https://api.example.com").unwrap();
1352 let client2 = HttpClient::new().with_base_url(base_url2).unwrap();
1353
1354 let tool_metadata = crate::ToolMetadata {
1355 name: "test".to_string(),
1356 title: None,
1357 description: Some("test".to_string()),
1358 parameters: json!({}),
1359 output_schema: None,
1360 method: "GET".to_string(),
1361 path: "/pets".to_string(),
1362 security: None,
1363 parameter_mappings: std::collections::HashMap::new(),
1364 };
1365
1366 let extracted_params = ExtractedParameters {
1367 path: HashMap::new(),
1368 query: HashMap::new(),
1369 headers: HashMap::new(),
1370 cookies: HashMap::new(),
1371 body: HashMap::new(),
1372 config: crate::tool_generator::RequestConfig::default(),
1373 };
1374
1375 let url1 = client1
1376 .build_url(&tool_metadata, &extracted_params)
1377 .unwrap();
1378 let url2 = client2
1379 .build_url(&tool_metadata, &extracted_params)
1380 .unwrap();
1381
1382 assert_eq!(url1.to_string(), "https://api.example.com/pets");
1384 assert_eq!(url2.to_string(), "https://api.example.com/pets");
1385 }
1386
1387 #[test]
1388 fn test_explode_array_parameters() {
1389 let base_url = Url::parse("https://api.example.com").unwrap();
1390 let client = HttpClient::new().with_base_url(base_url).unwrap();
1391
1392 let tool_metadata = crate::ToolMetadata {
1393 name: "test".to_string(),
1394 title: None,
1395 description: Some("test".to_string()),
1396 parameters: json!({}),
1397 output_schema: None,
1398 method: "GET".to_string(),
1399 path: "/search".to_string(),
1400 security: None,
1401 parameter_mappings: std::collections::HashMap::new(),
1402 };
1403
1404 let mut query_params_exploded = HashMap::new();
1406 query_params_exploded.insert(
1407 "include".to_string(),
1408 QueryParameter::new(json!(["asset", "scenes"]), true),
1409 );
1410
1411 let extracted_params_exploded = ExtractedParameters {
1412 path: HashMap::new(),
1413 query: query_params_exploded,
1414 headers: HashMap::new(),
1415 cookies: HashMap::new(),
1416 body: HashMap::new(),
1417 config: crate::tool_generator::RequestConfig::default(),
1418 };
1419
1420 let mut url_exploded = client
1421 .build_url(&tool_metadata, &extracted_params_exploded)
1422 .unwrap();
1423 HttpClient::add_query_parameters(&mut url_exploded, &extracted_params_exploded.query);
1424 let url_exploded_string = url_exploded.to_string();
1425
1426 let mut query_params_not_exploded = HashMap::new();
1428 query_params_not_exploded.insert(
1429 "include".to_string(),
1430 QueryParameter::new(json!(["asset", "scenes"]), false),
1431 );
1432
1433 let extracted_params_not_exploded = ExtractedParameters {
1434 path: HashMap::new(),
1435 query: query_params_not_exploded,
1436 headers: HashMap::new(),
1437 cookies: HashMap::new(),
1438 body: HashMap::new(),
1439 config: crate::tool_generator::RequestConfig::default(),
1440 };
1441
1442 let mut url_not_exploded = client
1443 .build_url(&tool_metadata, &extracted_params_not_exploded)
1444 .unwrap();
1445 HttpClient::add_query_parameters(
1446 &mut url_not_exploded,
1447 &extracted_params_not_exploded.query,
1448 );
1449 let url_not_exploded_string = url_not_exploded.to_string();
1450
1451 assert!(url_exploded_string.contains("include=asset"));
1453 assert!(url_exploded_string.contains("include=scenes"));
1454
1455 assert!(url_not_exploded_string.contains("include=asset%2Cscenes")); assert_ne!(url_exploded_string, url_not_exploded_string);
1460
1461 println!("Exploded URL: {url_exploded_string}");
1462 println!("Non-exploded URL: {url_not_exploded_string}");
1463 }
1464
1465 #[test]
1466 fn test_deep_object_parameters() {
1467 let base_url = Url::parse("https://api.example.com").unwrap();
1468 let client = HttpClient::new().with_base_url(base_url).unwrap();
1469
1470 let tool_metadata = crate::ToolMetadata {
1471 name: "test".to_string(),
1472 title: None,
1473 description: Some("test".to_string()),
1474 parameters: json!({}),
1475 output_schema: None,
1476 method: "GET".to_string(),
1477 path: "/search".to_string(),
1478 security: None,
1479 parameter_mappings: std::collections::HashMap::new(),
1480 };
1481
1482 let mut query_params = HashMap::new();
1484 query_params.insert(
1485 "fields".to_string(),
1486 QueryParameter::new(
1487 json!({ "camera": ["name", "type"], "node": ["name"] }),
1488 true,
1489 ),
1490 );
1491
1492 let extracted_params = ExtractedParameters {
1493 path: HashMap::new(),
1494 query: query_params,
1495 headers: HashMap::new(),
1496 cookies: HashMap::new(),
1497 body: HashMap::new(),
1498 config: crate::tool_generator::RequestConfig::default(),
1499 };
1500
1501 let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1502 HttpClient::add_query_parameters(&mut url, &extracted_params.query);
1503 let url_string = url.to_string();
1504
1505 assert!(
1508 url_string.contains("fields%5Bcamera%5D=name%2Ctype"),
1509 "expected fields[camera]=name,type, got {url_string}"
1510 );
1511 assert!(
1512 url_string.contains("fields%5Bnode%5D=name"),
1513 "expected fields[node]=name, got {url_string}"
1514 );
1515 assert!(
1517 !url_string.contains("%7B"),
1518 "object must not be JSON-serialized, got {url_string}"
1519 );
1520 }
1521
1522 #[test]
1523 fn test_is_image_helper() {
1524 let response_png = HttpResponse {
1526 status_code: 200,
1527 status_text: "OK".to_string(),
1528 headers: HashMap::new(),
1529 content_type: Some("image/png".to_string()),
1530 body: String::new(),
1531 body_bytes: None,
1532 is_success: true,
1533 request_method: "GET".to_string(),
1534 request_url: "http://example.com".to_string(),
1535 request_body: String::new(),
1536 };
1537 assert!(response_png.is_image());
1538
1539 let response_jpeg = HttpResponse {
1540 content_type: Some("image/jpeg".to_string()),
1541 ..response_png.clone()
1542 };
1543 assert!(response_jpeg.is_image());
1544
1545 let response_with_charset = HttpResponse {
1547 content_type: Some("image/png; charset=utf-8".to_string()),
1548 ..response_png.clone()
1549 };
1550 assert!(response_with_charset.is_image());
1551
1552 let response_json = HttpResponse {
1554 content_type: Some("application/json".to_string()),
1555 ..response_png.clone()
1556 };
1557 assert!(!response_json.is_image());
1558
1559 let response_text = HttpResponse {
1560 content_type: Some("text/plain".to_string()),
1561 ..response_png.clone()
1562 };
1563 assert!(!response_text.is_image());
1564
1565 let response_no_ct = HttpResponse {
1567 content_type: None,
1568 ..response_png
1569 };
1570 assert!(!response_no_ct.is_image());
1571 }
1572
1573 #[test]
1574 fn test_is_binary_helper() {
1575 let base_response = HttpResponse {
1576 status_code: 200,
1577 status_text: "OK".to_string(),
1578 headers: HashMap::new(),
1579 content_type: None,
1580 body: String::new(),
1581 body_bytes: None,
1582 is_success: true,
1583 request_method: "GET".to_string(),
1584 request_url: "http://example.com".to_string(),
1585 request_body: String::new(),
1586 };
1587
1588 let response_image = HttpResponse {
1590 content_type: Some("image/png".to_string()),
1591 ..base_response.clone()
1592 };
1593 assert!(response_image.is_binary());
1594
1595 let response_audio = HttpResponse {
1597 content_type: Some("audio/mpeg".to_string()),
1598 ..base_response.clone()
1599 };
1600 assert!(response_audio.is_binary());
1601
1602 let response_video = HttpResponse {
1604 content_type: Some("video/mp4".to_string()),
1605 ..base_response.clone()
1606 };
1607 assert!(response_video.is_binary());
1608
1609 let response_json = HttpResponse {
1611 content_type: Some("application/json".to_string()),
1612 ..base_response.clone()
1613 };
1614 assert!(!response_json.is_binary());
1615
1616 assert!(!base_response.is_binary());
1618 }
1619
1620 #[test]
1621 fn test_parse_data_uri_valid_png() {
1622 let uri = "data:image/png;base64,aGVsbG8=";
1624 let result = super::parse_data_uri(uri, "test_field").unwrap();
1625
1626 assert_eq!(result.mime_type, "image/png");
1627 assert_eq!(result.bytes, b"hello");
1628 }
1629
1630 #[test]
1631 fn test_parse_data_uri_valid_jpeg() {
1632 let uri = "data:image/jpeg;base64,d29ybGQ=";
1634 let result = super::parse_data_uri(uri, "image").unwrap();
1635
1636 assert_eq!(result.mime_type, "image/jpeg");
1637 assert_eq!(result.bytes, b"world");
1638 }
1639
1640 #[test]
1641 fn test_parse_data_uri_valid_application_json() {
1642 let uri = "data:application/json;base64,e30=";
1644 let result = super::parse_data_uri(uri, "data").unwrap();
1645
1646 assert_eq!(result.mime_type, "application/json");
1647 assert_eq!(result.bytes, b"{}");
1648 }
1649
1650 #[test]
1651 fn test_parse_data_uri_missing_data_prefix() {
1652 let uri = "image/png;base64,aGVsbG8=";
1653 let result = super::parse_data_uri(uri, "test_field");
1654
1655 assert!(result.is_err());
1656 let err = result.unwrap_err().to_string();
1657 assert!(err.contains("Invalid data URI format"));
1658 assert!(err.contains("test_field"));
1659 assert!(err.contains("expected 'data:<mime>;base64,<content>'"));
1660 }
1661
1662 #[test]
1663 fn test_parse_data_uri_missing_semicolon() {
1664 let uri = "data:image/png,aGVsbG8=";
1665 let result = super::parse_data_uri(uri, "my_image");
1666
1667 assert!(result.is_err());
1668 let err = result.unwrap_err().to_string();
1669 assert!(err.contains("Invalid data URI format"));
1670 assert!(err.contains("my_image"));
1671 }
1672
1673 #[test]
1674 fn test_parse_data_uri_missing_comma() {
1675 let uri = "data:image/png;base64aGVsbG8=";
1676 let result = super::parse_data_uri(uri, "field");
1677
1678 assert!(result.is_err());
1679 let err = result.unwrap_err().to_string();
1680 assert!(err.contains("Invalid data URI format"));
1681 }
1682
1683 #[test]
1684 fn test_parse_data_uri_unsupported_encoding() {
1685 let uri = "data:image/png;ascii,hello";
1686 let result = super::parse_data_uri(uri, "test_field");
1687
1688 assert!(result.is_err());
1689 let err = result.unwrap_err().to_string();
1690 assert!(err.contains("Unsupported encoding 'ascii'"));
1691 assert!(err.contains("test_field"));
1692 assert!(err.contains("only base64 is supported"));
1693 }
1694
1695 #[test]
1696 fn test_parse_data_uri_unsupported_encoding_utf8() {
1697 let uri = "data:text/plain;utf-8,hello world";
1698 let result = super::parse_data_uri(uri, "content");
1699
1700 assert!(result.is_err());
1701 let err = result.unwrap_err().to_string();
1702 assert!(err.contains("Unsupported encoding 'utf-8'"));
1703 assert!(err.contains("content"));
1704 }
1705
1706 #[test]
1707 fn test_parse_data_uri_invalid_base64() {
1708 let uri = "data:image/png;base64,not-valid-base64!!!";
1710 let result = super::parse_data_uri(uri, "bad_image");
1711
1712 assert!(result.is_err());
1713 let err = result.unwrap_err().to_string();
1714 assert!(err.contains("Invalid base64 content"));
1715 assert!(err.contains("bad_image"));
1716 }
1717
1718 #[test]
1719 fn test_parse_data_uri_empty_content() {
1720 let uri = "data:application/octet-stream;base64,";
1722 let result = super::parse_data_uri(uri, "empty").unwrap();
1723
1724 assert_eq!(result.mime_type, "application/octet-stream");
1725 assert!(result.bytes.is_empty());
1726 }
1727
1728 #[test]
1729 fn test_parse_data_uri_complex_mime_type() {
1730 let uri = "data:application/vnd.api+json;base64,e30=";
1732 let result = super::parse_data_uri(uri, "api_data").unwrap();
1733
1734 assert_eq!(result.mime_type, "application/vnd.api+json");
1735 assert_eq!(result.bytes, b"{}");
1736 }
1737
1738 #[test]
1739 fn test_parse_data_uri_mime_type_with_parameters() {
1740 let uri = "data:text/plain;charset=utf-8;base64,SGVsbG8gV29ybGQ=";
1742 let result = super::parse_data_uri(uri, "text_field").unwrap();
1743
1744 assert_eq!(result.mime_type, "text/plain;charset=utf-8");
1745 assert_eq!(result.bytes, b"Hello World");
1746 }
1747
1748 #[test]
1749 fn test_parse_data_uri_mime_type_with_multiple_parameters() {
1750 let uri = "data:text/html;charset=utf-8;boundary=something;base64,PGh0bWw+";
1752 let result = super::parse_data_uri(uri, "html_field").unwrap();
1753
1754 assert_eq!(
1755 result.mime_type,
1756 "text/html;charset=utf-8;boundary=something"
1757 );
1758 assert_eq!(result.bytes, b"<html>");
1759 }
1760
1761 #[test]
1762 fn test_parse_data_uri_empty_mime_type() {
1763 let uri = "data:;base64,SGVsbG8=";
1765 let result = super::parse_data_uri(uri, "field");
1766
1767 assert!(result.is_err());
1768 let err = result.unwrap_err().to_string();
1769 assert!(err.contains("MIME type cannot be empty"));
1770 }
1771
1772 #[test]
1773 fn test_parse_data_uri_empty_string() {
1774 let result = super::parse_data_uri("", "field");
1775
1776 assert!(result.is_err());
1777 let err = result.unwrap_err().to_string();
1778 assert!(err.contains("Invalid data URI format"));
1779 }
1780
1781 #[test]
1782 fn test_parse_data_uri_just_data_prefix() {
1783 let result = super::parse_data_uri("data:", "field");
1784
1785 assert!(result.is_err());
1786 let err = result.unwrap_err().to_string();
1787 assert!(err.contains("Invalid data URI format"));
1788 }
1789
1790 #[test]
1802 fn test_add_request_body_multipart_with_valid_file() {
1803 let client = HttpClient::new();
1804 let request = client.client.post("http://example.com/upload");
1805
1806 let mut body = HashMap::new();
1807 body.insert(
1809 "file".to_string(),
1810 json!({
1811 "content": "data:image/png;base64,iVBORw0KGgo=",
1812 "filename": "test.png"
1813 }),
1814 );
1815 body.insert("description".to_string(), json!("Test file upload"));
1817
1818 let config = crate::tool_generator::RequestConfig {
1819 timeout_seconds: 30,
1820 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1821 };
1822
1823 let result = HttpClient::add_request_body(request, &body, &config);
1824 assert!(
1825 result.is_ok(),
1826 "Should successfully build multipart form with valid file"
1827 );
1828 }
1829
1830 #[test]
1831 fn test_add_request_body_multipart_with_invalid_data_uri() {
1832 let client = HttpClient::new();
1833 let request = client.client.post("http://example.com/upload");
1834
1835 let mut body = HashMap::new();
1836 body.insert(
1838 "file".to_string(),
1839 json!({
1840 "content": "data:image/png,notbase64",
1841 "filename": "test.png"
1842 }),
1843 );
1844
1845 let config = crate::tool_generator::RequestConfig {
1846 timeout_seconds: 30,
1847 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1848 };
1849
1850 let result = HttpClient::add_request_body(request, &body, &config);
1851 assert!(result.is_err(), "Should fail with invalid data URI");
1852 let err = result.unwrap_err().to_string();
1853 assert!(
1854 err.contains("Invalid data URI format"),
1855 "Error should mention invalid format"
1856 );
1857 }
1858
1859 #[test]
1860 fn test_add_request_body_multipart_with_invalid_base64() {
1861 let client = HttpClient::new();
1862 let request = client.client.post("http://example.com/upload");
1863
1864 let mut body = HashMap::new();
1865 body.insert(
1867 "file".to_string(),
1868 json!({
1869 "content": "data:image/png;base64,!!!invalid!!!",
1870 "filename": "test.png"
1871 }),
1872 );
1873
1874 let config = crate::tool_generator::RequestConfig {
1875 timeout_seconds: 30,
1876 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1877 };
1878
1879 let result = HttpClient::add_request_body(request, &body, &config);
1880 assert!(result.is_err(), "Should fail with invalid base64");
1881 let err = result.unwrap_err().to_string();
1882 assert!(
1883 err.contains("Invalid base64 content"),
1884 "Error should mention invalid base64"
1885 );
1886 }
1887
1888 #[test]
1889 fn test_add_request_body_multipart_text_only() {
1890 let client = HttpClient::new();
1891 let request = client.client.post("http://example.com/upload");
1892
1893 let mut body = HashMap::new();
1894 body.insert("field1".to_string(), json!("text value"));
1895 body.insert("field2".to_string(), json!(123));
1896 body.insert("field3".to_string(), json!(true));
1897
1898 let config = crate::tool_generator::RequestConfig {
1899 timeout_seconds: 30,
1900 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1901 };
1902
1903 let result = HttpClient::add_request_body(request, &body, &config);
1904 assert!(
1905 result.is_ok(),
1906 "Should successfully build multipart form with text-only fields"
1907 );
1908 }
1909
1910 #[test]
1911 fn test_add_request_body_multipart_mixed_content() {
1912 let client = HttpClient::new();
1913 let request = client.client.post("http://example.com/upload");
1914
1915 let mut body = HashMap::new();
1916 body.insert(
1918 "image".to_string(),
1919 json!({
1920 "content": "data:image/jpeg;base64,/9j/4AAQ",
1921 "filename": "photo.jpg"
1922 }),
1923 );
1924 body.insert("title".to_string(), json!("My Photo"));
1926 body.insert("tags".to_string(), json!(["nature", "sunset"]));
1927
1928 let config = crate::tool_generator::RequestConfig {
1929 timeout_seconds: 30,
1930 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1931 };
1932
1933 let result = HttpClient::add_request_body(request, &body, &config);
1934 assert!(result.is_ok(), "Should handle mixed file and text content");
1935 }
1936
1937 #[test]
1938 fn test_add_request_body_multipart_without_filename() {
1939 let client = HttpClient::new();
1940 let request = client.client.post("http://example.com/upload");
1941
1942 let mut body = HashMap::new();
1943 body.insert(
1945 "upload".to_string(),
1946 json!({
1947 "content": "data:application/pdf;base64,JVBERi0="
1948 }),
1949 );
1950
1951 let config = crate::tool_generator::RequestConfig {
1952 timeout_seconds: 30,
1953 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1954 };
1955
1956 let result = HttpClient::add_request_body(request, &body, &config);
1957 assert!(
1958 result.is_ok(),
1959 "Should handle file upload without explicit filename"
1960 );
1961 }
1962
1963 #[test]
1964 fn test_add_request_body_json() {
1965 let client = HttpClient::new();
1966 let request = client.client.post("http://example.com/api");
1967
1968 let mut body = HashMap::new();
1969 body.insert("name".to_string(), json!("test"));
1970 body.insert("value".to_string(), json!(42));
1971
1972 let config = crate::tool_generator::RequestConfig {
1973 timeout_seconds: 30,
1974 content_type: mime::APPLICATION_JSON.to_string(),
1975 };
1976
1977 let result = HttpClient::add_request_body(request, &body, &config);
1978 assert!(result.is_ok(), "Should build JSON body");
1979 }
1980
1981 #[test]
1982 fn test_add_request_body_form_urlencoded() {
1983 let client = HttpClient::new();
1984 let request = client.client.post("http://example.com/form");
1985
1986 let mut body = HashMap::new();
1987 body.insert("username".to_string(), json!("user"));
1988 body.insert("password".to_string(), json!("secret"));
1989
1990 let config = crate::tool_generator::RequestConfig {
1991 timeout_seconds: 30,
1992 content_type: mime::APPLICATION_WWW_FORM_URLENCODED.to_string(),
1993 };
1994
1995 let result = HttpClient::add_request_body(request, &body, &config);
1996 assert!(result.is_ok(), "Should build form-urlencoded body");
1997 }
1998
1999 #[tokio::test]
2000 async fn http_client_with_insecure_still_serves_plain_http() {
2001 let mut server = mockito::Server::new_async().await;
2002 let mock = server
2003 .mock("GET", "/ping")
2004 .with_status(200)
2005 .with_body("pong")
2006 .create_async()
2007 .await;
2008
2009 let base_url: Url = server.url().parse().unwrap();
2010 let client = HttpClient::new()
2011 .with_insecure(true)
2012 .with_base_url(base_url.clone())
2013 .unwrap();
2014
2015 let url = base_url.join("ping").unwrap();
2016 let response = client.client.get(url).send().await.unwrap();
2017 assert_eq!(response.status().as_u16(), 200);
2018 assert_eq!(response.text().await.unwrap(), "pong");
2019
2020 mock.assert_async().await;
2021 }
2022
2023 #[test]
2024 fn test_add_request_body_empty() {
2025 let client = HttpClient::new();
2026 let request = client.client.post("http://example.com/api");
2027
2028 let body = HashMap::new();
2029
2030 let config = crate::tool_generator::RequestConfig::default();
2031
2032 let result = HttpClient::add_request_body(request, &body, &config);
2033 assert!(result.is_ok(), "Should handle empty body");
2034 }
2035}