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 {
540 let mut query_pairs = url.query_pairs_mut();
541 for (key, query_param) in query_params {
542 if let Value::Array(arr) = &query_param.value {
543 if query_param.explode {
544 for item in arr {
546 let item_str = match item {
547 Value::String(s) => s.clone(),
548 Value::Number(n) => n.to_string(),
549 Value::Bool(b) => b.to_string(),
550 _ => item.to_string(),
551 };
552 query_pairs.append_pair(key, &item_str);
553 }
554 } else {
555 let array_values: Vec<String> = arr
557 .iter()
558 .map(|item| match item {
559 Value::String(s) => s.clone(),
560 Value::Number(n) => n.to_string(),
561 Value::Bool(b) => b.to_string(),
562 _ => item.to_string(),
563 })
564 .collect();
565 let comma_separated = array_values.join(",");
566 query_pairs.append_pair(key, &comma_separated);
567 }
568 } else {
569 let value_str = match &query_param.value {
570 Value::String(s) => s.clone(),
571 Value::Number(n) => n.to_string(),
572 Value::Bool(b) => b.to_string(),
573 _ => query_param.value.to_string(),
574 };
575 query_pairs.append_pair(key, &value_str);
576 }
577 }
578 }
579 }
580
581 fn add_headers_from_map(mut request: RequestBuilder, headers: &HeaderMap) -> RequestBuilder {
583 for (key, value) in headers {
584 request = request.header(key, value);
586 }
587 request
588 }
589
590 fn add_headers(
592 mut request: RequestBuilder,
593 headers: &HashMap<String, Value>,
594 ) -> RequestBuilder {
595 for (key, value) in headers {
596 let value_str = match value {
597 Value::String(s) => s.clone(),
598 Value::Number(n) => n.to_string(),
599 Value::Bool(b) => b.to_string(),
600 _ => value.to_string(),
601 };
602 request = request.header(key, value_str);
603 }
604 request
605 }
606
607 fn add_cookies(
609 mut request: RequestBuilder,
610 cookies: &HashMap<String, Value>,
611 ) -> RequestBuilder {
612 if !cookies.is_empty() {
613 let cookie_header = cookies
614 .iter()
615 .map(|(key, value)| {
616 let value_str = match value {
617 Value::String(s) => s.clone(),
618 Value::Number(n) => n.to_string(),
619 Value::Bool(b) => b.to_string(),
620 _ => value.to_string(),
621 };
622 format!("{key}={value_str}")
623 })
624 .collect::<Vec<_>>()
625 .join("; ");
626
627 request = request.header(header::COOKIE, cookie_header);
628 }
629 request
630 }
631
632 fn add_request_body(
634 mut request: RequestBuilder,
635 body: &HashMap<String, Value>,
636 config: &crate::tool_generator::RequestConfig,
637 ) -> Result<RequestBuilder, Error> {
638 if body.is_empty() {
639 return Ok(request);
640 }
641
642 match config.content_type.as_str() {
644 s if s == mime::APPLICATION_JSON.as_ref() => {
645 request = request.header(header::CONTENT_TYPE, &config.content_type);
647
648 if body.len() == 1 && body.contains_key("request_body") {
650 let body_value = &body["request_body"];
652 let json_string = serde_json::to_string(body_value).map_err(|e| {
653 Error::Http(format!("Failed to serialize request body: {e}"))
654 })?;
655 request = request.body(json_string);
656 } else {
657 let body_object =
659 Value::Object(body.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
660 let json_string = serde_json::to_string(&body_object).map_err(|e| {
661 Error::Http(format!("Failed to serialize request body: {e}"))
662 })?;
663 request = request.body(json_string);
664 }
665 }
666 s if s == mime::APPLICATION_WWW_FORM_URLENCODED.as_ref() => {
667 request = request.header(header::CONTENT_TYPE, &config.content_type);
669
670 let form_data: Vec<(String, String)> = body
672 .iter()
673 .map(|(key, value)| {
674 let value_str = match value {
675 Value::String(s) => s.clone(),
676 Value::Number(n) => n.to_string(),
677 Value::Bool(b) => b.to_string(),
678 _ => value.to_string(),
679 };
680 (key.clone(), value_str)
681 })
682 .collect();
683 request = request.form(&form_data);
684 }
685 s if s == mime::MULTIPART_FORM_DATA.as_ref() => {
686 let mut form = reqwest::multipart::Form::new();
688
689 for (key, value) in body {
690 if let Some(obj) = value.as_object()
692 && let Some(content_value) = obj.get("content")
693 && let Some(content_str) = content_value.as_str()
694 && content_str.starts_with("data:")
695 {
696 let data_uri = parse_data_uri(content_str, key)?;
698
699 let filename = obj
701 .get("filename")
702 .and_then(|v| v.as_str())
703 .unwrap_or("file")
704 .to_string();
705
706 let part = reqwest::multipart::Part::bytes(data_uri.bytes)
708 .file_name(filename)
709 .mime_str(&data_uri.mime_type)
710 .map_err(|e| Error::Http(format!("Invalid MIME type: {e}")))?;
711
712 form = form.part(key.clone(), part);
713 continue;
714 }
715
716 let text_value = match value {
718 Value::String(s) => s.clone(),
719 Value::Number(n) => n.to_string(),
720 Value::Bool(b) => b.to_string(),
721 _ => value.to_string(),
722 };
723 form = form.text(key.clone(), text_value);
724 }
725
726 request = request.multipart(form);
727 }
728 _ => {
729 request = request.header(header::CONTENT_TYPE, &config.content_type);
731
732 let body_object =
734 Value::Object(body.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
735 let json_string = serde_json::to_string(&body_object)
736 .map_err(|e| Error::Http(format!("Failed to serialize request body: {e}")))?;
737 request = request.body(json_string);
738 }
739 }
740
741 Ok(request)
742 }
743
744 async fn process_response_with_request(
746 &self,
747 response: reqwest::Response,
748 method: &str,
749 url: &str,
750 request_body: &str,
751 ) -> Result<HttpResponse, Error> {
752 let status = response.status();
753
754 let content_type = response
756 .headers()
757 .get(header::CONTENT_TYPE)
758 .and_then(|v| v.to_str().ok())
759 .map(|s| s.to_string());
760
761 let is_binary_content = content_type
763 .as_ref()
764 .and_then(|ct| ct.parse::<mime::Mime>().ok())
765 .map(|mime_type| matches!(mime_type.type_(), mime::IMAGE | mime::AUDIO | mime::VIDEO))
766 .unwrap_or(false);
767
768 let headers = response
769 .headers()
770 .iter()
771 .map(|(name, value)| {
772 (
773 name.to_string(),
774 value.to_str().unwrap_or("<invalid>").to_string(),
775 )
776 })
777 .collect();
778
779 let (body, body_bytes) = if is_binary_content {
781 let bytes = response
783 .bytes()
784 .await
785 .map_err(|e| Error::Http(format!("Failed to read response body: {e}")))?;
786
787 let body_text = format!(
789 "[Binary content: {} bytes, Content-Type: {}]",
790 bytes.len(),
791 content_type.as_ref().unwrap_or(&"unknown".to_string())
792 );
793
794 (body_text, Some(bytes.to_vec()))
795 } else {
796 let text = response
798 .text()
799 .await
800 .map_err(|e| Error::Http(format!("Failed to read response body: {e}")))?;
801
802 (text, None)
803 };
804
805 let is_success = status.is_success();
806 let status_code = status.as_u16();
807 let status_text = status.canonical_reason().unwrap_or("Unknown").to_string();
808
809 let enhanced_status_text = match status {
811 StatusCode::BAD_REQUEST => {
812 format!("{status_text} - Bad Request: Check request parameters")
813 }
814 StatusCode::UNAUTHORIZED => {
815 format!("{status_text} - Unauthorized: Authentication required")
816 }
817 StatusCode::FORBIDDEN => format!("{status_text} - Forbidden: Access denied"),
818 StatusCode::NOT_FOUND => {
819 format!("{status_text} - Not Found: Endpoint or resource does not exist")
820 }
821 StatusCode::METHOD_NOT_ALLOWED => format!(
822 "{} - Method Not Allowed: {} method not supported",
823 status_text,
824 method.to_uppercase()
825 ),
826 StatusCode::UNPROCESSABLE_ENTITY => {
827 format!("{status_text} - Unprocessable Entity: Request validation failed")
828 }
829 StatusCode::TOO_MANY_REQUESTS => {
830 format!("{status_text} - Too Many Requests: Rate limit exceeded")
831 }
832 StatusCode::INTERNAL_SERVER_ERROR => {
833 format!("{status_text} - Internal Server Error: Server encountered an error")
834 }
835 StatusCode::BAD_GATEWAY => {
836 format!("{status_text} - Bad Gateway: Upstream server error")
837 }
838 StatusCode::SERVICE_UNAVAILABLE => {
839 format!("{status_text} - Service Unavailable: Server temporarily unavailable")
840 }
841 StatusCode::GATEWAY_TIMEOUT => {
842 format!("{status_text} - Gateway Timeout: Upstream server timeout")
843 }
844 _ => status_text,
845 };
846
847 Ok(HttpResponse {
848 status_code,
849 status_text: enhanced_status_text,
850 headers,
851 content_type,
852 body,
853 body_bytes,
854 is_success,
855 request_method: method.to_string(),
856 request_url: url.to_string(),
857 request_body: request_body.to_string(),
858 })
859 }
860}
861
862impl Default for HttpClient {
863 fn default() -> Self {
864 Self::new()
865 }
866}
867
868#[derive(Debug, Clone)]
870pub struct HttpResponse {
871 pub status_code: u16,
872 pub status_text: String,
873 pub headers: HashMap<String, String>,
874 pub content_type: Option<String>,
875 pub body: String,
876 pub body_bytes: Option<Vec<u8>>,
877 pub is_success: bool,
878 pub request_method: String,
879 pub request_url: String,
880 pub request_body: String,
881}
882
883impl HttpResponse {
884 pub fn json(&self) -> Result<Value, Error> {
890 serde_json::from_str(&self.body)
891 .map_err(|e| Error::Http(format!("Failed to parse response as JSON: {e}")))
892 }
893
894 #[must_use]
898 pub fn is_image(&self) -> bool {
899 self.content_type
900 .as_ref()
901 .and_then(|ct| ct.parse::<mime::Mime>().ok())
902 .map(|mime_type| mime_type.type_() == mime::IMAGE)
903 .unwrap_or(false)
904 }
905
906 #[must_use]
910 pub fn is_binary(&self) -> bool {
911 self.content_type
912 .as_ref()
913 .and_then(|ct| ct.parse::<mime::Mime>().ok())
914 .map(|mime_type| matches!(mime_type.type_(), mime::IMAGE | mime::AUDIO | mime::VIDEO))
915 .unwrap_or(false)
916 }
917
918 #[must_use]
920 pub fn to_mcp_content(&self) -> String {
921 let method = if self.request_method.is_empty() {
922 None
923 } else {
924 Some(self.request_method.as_str())
925 };
926 let url = if self.request_url.is_empty() {
927 None
928 } else {
929 Some(self.request_url.as_str())
930 };
931 let body = if self.request_body.is_empty() {
932 None
933 } else {
934 Some(self.request_body.as_str())
935 };
936 self.to_mcp_content_with_request(method, url, body)
937 }
938
939 pub fn to_mcp_content_with_request(
941 &self,
942 method: Option<&str>,
943 url: Option<&str>,
944 request_body: Option<&str>,
945 ) -> String {
946 let mut result = format!(
947 "HTTP {} {}\n\nStatus: {} {}\n",
948 if self.is_success { "✅" } else { "❌" },
949 if self.is_success { "Success" } else { "Error" },
950 self.status_code,
951 self.status_text
952 );
953
954 if let (Some(method), Some(url)) = (method, url) {
956 result.push_str("\nRequest: ");
957 result.push_str(&method.to_uppercase());
958 result.push(' ');
959 result.push_str(url);
960 result.push('\n');
961
962 if let Some(body) = request_body
963 && !body.is_empty()
964 && body != "{}"
965 {
966 result.push_str("\nRequest Body:\n");
967 if let Ok(parsed) = serde_json::from_str::<Value>(body) {
968 if let Ok(pretty) = serde_json::to_string_pretty(&parsed) {
969 result.push_str(&pretty);
970 } else {
971 result.push_str(body);
972 }
973 } else {
974 result.push_str(body);
975 }
976 result.push('\n');
977 }
978 }
979
980 if !self.headers.is_empty() {
982 result.push_str("\nHeaders:\n");
983 for (key, value) in &self.headers {
984 if [
986 header::CONTENT_TYPE.as_str(),
987 header::CONTENT_LENGTH.as_str(),
988 header::LOCATION.as_str(),
989 header::SET_COOKIE.as_str(),
990 ]
991 .iter()
992 .any(|&h| key.to_lowercase().contains(h))
993 {
994 result.push_str(" ");
995 result.push_str(key);
996 result.push_str(": ");
997 result.push_str(value);
998 result.push('\n');
999 }
1000 }
1001 }
1002
1003 result.push_str("\nResponse Body:\n");
1005 if self.body.is_empty() {
1006 result.push_str("(empty)");
1007 } else if let Ok(json_value) = self.json() {
1008 match serde_json::to_string_pretty(&json_value) {
1010 Ok(pretty) => result.push_str(&pretty),
1011 Err(_) => result.push_str(&self.body),
1012 }
1013 } else {
1014 if self.body.len() > 2000 {
1016 result.push_str(&self.body[..2000]);
1017 result.push_str("\n... (");
1018 result.push_str(&(self.body.len() - 2000).to_string());
1019 result.push_str(" more characters)");
1020 } else {
1021 result.push_str(&self.body);
1022 }
1023 }
1024
1025 result
1026 }
1027}
1028
1029#[cfg(test)]
1030mod tests {
1031 use super::*;
1032 use crate::tool_generator::ExtractedParameters;
1033 use serde_json::json;
1034 use std::collections::HashMap;
1035
1036 #[test]
1037 fn test_with_base_url_validation() {
1038 let url = Url::parse("https://api.example.com").unwrap();
1040 let client = HttpClient::new().with_base_url(url);
1041 assert!(client.is_ok());
1042
1043 let url = Url::parse("http://localhost:8080").unwrap();
1044 let client = HttpClient::new().with_base_url(url);
1045 assert!(client.is_ok());
1046
1047 assert!(Url::parse("not-a-url").is_err());
1049 assert!(Url::parse("").is_err());
1050
1051 let url = Url::parse("ftp://invalid-scheme.com").unwrap();
1053 let client = HttpClient::new().with_base_url(url);
1054 assert!(client.is_ok()); }
1056
1057 #[test]
1058 fn test_build_url_with_base_url() {
1059 let base_url = Url::parse("https://api.example.com").unwrap();
1060 let client = HttpClient::new().with_base_url(base_url).unwrap();
1061
1062 let tool_metadata = crate::ToolMetadata {
1063 name: "test".to_string(),
1064 title: None,
1065 description: Some("test".to_string()),
1066 parameters: json!({}),
1067 output_schema: None,
1068 method: "GET".to_string(),
1069 path: "/pets/{id}".to_string(),
1070 security: None,
1071 parameter_mappings: std::collections::HashMap::new(),
1072 };
1073
1074 let mut path_params = HashMap::new();
1075 path_params.insert("id".to_string(), json!(123));
1076
1077 let extracted_params = ExtractedParameters {
1078 path: path_params,
1079 query: HashMap::new(),
1080 headers: HashMap::new(),
1081 cookies: HashMap::new(),
1082 body: HashMap::new(),
1083 config: crate::tool_generator::RequestConfig::default(),
1084 };
1085
1086 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1087 assert_eq!(url.to_string(), "https://api.example.com/pets/123");
1088 }
1089
1090 #[test]
1091 fn test_build_url_with_base_url_containing_path() {
1092 let test_cases = vec![
1093 "https://api.example.com/api/v4",
1094 "https://api.example.com/api/v4/",
1095 ];
1096
1097 for base_url in test_cases {
1098 let base_url = Url::parse(base_url).unwrap();
1099 let client = HttpClient::new().with_base_url(base_url).unwrap();
1100
1101 let tool_metadata = crate::ToolMetadata {
1102 name: "test".to_string(),
1103 title: None,
1104 description: Some("test".to_string()),
1105 parameters: json!({}),
1106 output_schema: None,
1107 method: "GET".to_string(),
1108 path: "/pets/{id}".to_string(),
1109 security: None,
1110 parameter_mappings: std::collections::HashMap::new(),
1111 };
1112
1113 let mut path_params = HashMap::new();
1114 path_params.insert("id".to_string(), json!(123));
1115
1116 let extracted_params = ExtractedParameters {
1117 path: path_params,
1118 query: HashMap::new(),
1119 headers: HashMap::new(),
1120 cookies: HashMap::new(),
1121 body: HashMap::new(),
1122 config: crate::tool_generator::RequestConfig::default(),
1123 };
1124
1125 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1126 assert_eq!(url.to_string(), "https://api.example.com/api/v4/pets/123");
1127 }
1128 }
1129
1130 #[test]
1131 fn test_build_url_without_base_url() {
1132 let client = HttpClient::new();
1133
1134 let tool_metadata = crate::ToolMetadata {
1135 name: "test".to_string(),
1136 title: None,
1137 description: Some("test".to_string()),
1138 parameters: json!({}),
1139 output_schema: None,
1140 method: "GET".to_string(),
1141 path: "https://api.example.com/pets/123".to_string(),
1142 security: None,
1143 parameter_mappings: std::collections::HashMap::new(),
1144 };
1145
1146 let extracted_params = ExtractedParameters {
1147 path: HashMap::new(),
1148 query: HashMap::new(),
1149 headers: HashMap::new(),
1150 cookies: HashMap::new(),
1151 body: HashMap::new(),
1152 config: crate::tool_generator::RequestConfig::default(),
1153 };
1154
1155 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1156 assert_eq!(url.to_string(), "https://api.example.com/pets/123");
1157
1158 let tool_metadata_relative = crate::ToolMetadata {
1160 name: "test".to_string(),
1161 title: None,
1162 description: Some("test".to_string()),
1163 parameters: json!({}),
1164 output_schema: None,
1165 method: "GET".to_string(),
1166 path: "/pets/123".to_string(),
1167 security: None,
1168 parameter_mappings: std::collections::HashMap::new(),
1169 };
1170
1171 let result = client.build_url(&tool_metadata_relative, &extracted_params);
1172 assert!(result.is_err());
1173 assert!(
1174 result
1175 .unwrap_err()
1176 .to_string()
1177 .contains("No base URL configured")
1178 );
1179 }
1180
1181 #[test]
1182 fn test_query_parameter_encoding_integration() {
1183 let base_url = Url::parse("https://api.example.com").unwrap();
1184 let client = HttpClient::new().with_base_url(base_url).unwrap();
1185
1186 let tool_metadata = crate::ToolMetadata {
1187 name: "test".to_string(),
1188 title: None,
1189 description: Some("test".to_string()),
1190 parameters: json!({}),
1191 output_schema: None,
1192 method: "GET".to_string(),
1193 path: "/search".to_string(),
1194 security: None,
1195 parameter_mappings: std::collections::HashMap::new(),
1196 };
1197
1198 let mut query_params = HashMap::new();
1200 query_params.insert(
1201 "q".to_string(),
1202 QueryParameter::new(json!("hello world"), true),
1203 ); query_params.insert(
1205 "category".to_string(),
1206 QueryParameter::new(json!("pets&dogs"), true),
1207 ); query_params.insert(
1209 "special".to_string(),
1210 QueryParameter::new(json!("foo=bar"), true),
1211 ); query_params.insert(
1213 "unicode".to_string(),
1214 QueryParameter::new(json!("café"), true),
1215 ); query_params.insert(
1217 "percent".to_string(),
1218 QueryParameter::new(json!("100%"), true),
1219 ); let extracted_params = ExtractedParameters {
1222 path: HashMap::new(),
1223 query: query_params,
1224 headers: HashMap::new(),
1225 cookies: HashMap::new(),
1226 body: HashMap::new(),
1227 config: crate::tool_generator::RequestConfig::default(),
1228 };
1229
1230 let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1231 HttpClient::add_query_parameters(&mut url, &extracted_params.query);
1232
1233 let url_string = url.to_string();
1234
1235 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")); }
1243
1244 #[test]
1245 fn test_array_query_parameters() {
1246 let base_url = Url::parse("https://api.example.com").unwrap();
1247 let client = HttpClient::new().with_base_url(base_url).unwrap();
1248
1249 let tool_metadata = crate::ToolMetadata {
1250 name: "test".to_string(),
1251 title: None,
1252 description: Some("test".to_string()),
1253 parameters: json!({}),
1254 output_schema: None,
1255 method: "GET".to_string(),
1256 path: "/search".to_string(),
1257 security: None,
1258 parameter_mappings: std::collections::HashMap::new(),
1259 };
1260
1261 let mut query_params = HashMap::new();
1262 query_params.insert(
1263 "status".to_string(),
1264 QueryParameter::new(json!(["available", "pending"]), true),
1265 );
1266 query_params.insert(
1267 "tags".to_string(),
1268 QueryParameter::new(json!(["red & blue", "fast=car"]), true),
1269 );
1270
1271 let extracted_params = ExtractedParameters {
1272 path: HashMap::new(),
1273 query: query_params,
1274 headers: HashMap::new(),
1275 cookies: HashMap::new(),
1276 body: HashMap::new(),
1277 config: crate::tool_generator::RequestConfig::default(),
1278 };
1279
1280 let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1281 HttpClient::add_query_parameters(&mut url, &extracted_params.query);
1282
1283 let url_string = url.to_string();
1284
1285 assert!(url_string.contains("status=available"));
1287 assert!(url_string.contains("status=pending"));
1288 assert!(url_string.contains("tags=red+%26+blue")); assert!(url_string.contains("tags=fast%3Dcar")); }
1291
1292 #[test]
1293 fn test_path_parameter_substitution() {
1294 let base_url = Url::parse("https://api.example.com").unwrap();
1295 let client = HttpClient::new().with_base_url(base_url).unwrap();
1296
1297 let tool_metadata = crate::ToolMetadata {
1298 name: "test".to_string(),
1299 title: None,
1300 description: Some("test".to_string()),
1301 parameters: json!({}),
1302 output_schema: None,
1303 method: "GET".to_string(),
1304 path: "/users/{userId}/pets/{petId}".to_string(),
1305 security: None,
1306 parameter_mappings: std::collections::HashMap::new(),
1307 };
1308
1309 let mut path_params = HashMap::new();
1310 path_params.insert("userId".to_string(), json!(42));
1311 path_params.insert("petId".to_string(), json!("special-pet-123"));
1312
1313 let extracted_params = ExtractedParameters {
1314 path: path_params,
1315 query: HashMap::new(),
1316 headers: HashMap::new(),
1317 cookies: HashMap::new(),
1318 body: HashMap::new(),
1319 config: crate::tool_generator::RequestConfig::default(),
1320 };
1321
1322 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1323 assert_eq!(
1324 url.to_string(),
1325 "https://api.example.com/users/42/pets/special-pet-123"
1326 );
1327 }
1328
1329 #[test]
1330 fn test_url_join_edge_cases() {
1331 let base_url1 = Url::parse("https://api.example.com/").unwrap();
1333 let client1 = HttpClient::new().with_base_url(base_url1).unwrap();
1334
1335 let base_url2 = Url::parse("https://api.example.com").unwrap();
1336 let client2 = HttpClient::new().with_base_url(base_url2).unwrap();
1337
1338 let tool_metadata = crate::ToolMetadata {
1339 name: "test".to_string(),
1340 title: None,
1341 description: Some("test".to_string()),
1342 parameters: json!({}),
1343 output_schema: None,
1344 method: "GET".to_string(),
1345 path: "/pets".to_string(),
1346 security: None,
1347 parameter_mappings: std::collections::HashMap::new(),
1348 };
1349
1350 let extracted_params = ExtractedParameters {
1351 path: HashMap::new(),
1352 query: HashMap::new(),
1353 headers: HashMap::new(),
1354 cookies: HashMap::new(),
1355 body: HashMap::new(),
1356 config: crate::tool_generator::RequestConfig::default(),
1357 };
1358
1359 let url1 = client1
1360 .build_url(&tool_metadata, &extracted_params)
1361 .unwrap();
1362 let url2 = client2
1363 .build_url(&tool_metadata, &extracted_params)
1364 .unwrap();
1365
1366 assert_eq!(url1.to_string(), "https://api.example.com/pets");
1368 assert_eq!(url2.to_string(), "https://api.example.com/pets");
1369 }
1370
1371 #[test]
1372 fn test_explode_array_parameters() {
1373 let base_url = Url::parse("https://api.example.com").unwrap();
1374 let client = HttpClient::new().with_base_url(base_url).unwrap();
1375
1376 let tool_metadata = crate::ToolMetadata {
1377 name: "test".to_string(),
1378 title: None,
1379 description: Some("test".to_string()),
1380 parameters: json!({}),
1381 output_schema: None,
1382 method: "GET".to_string(),
1383 path: "/search".to_string(),
1384 security: None,
1385 parameter_mappings: std::collections::HashMap::new(),
1386 };
1387
1388 let mut query_params_exploded = HashMap::new();
1390 query_params_exploded.insert(
1391 "include".to_string(),
1392 QueryParameter::new(json!(["asset", "scenes"]), true),
1393 );
1394
1395 let extracted_params_exploded = ExtractedParameters {
1396 path: HashMap::new(),
1397 query: query_params_exploded,
1398 headers: HashMap::new(),
1399 cookies: HashMap::new(),
1400 body: HashMap::new(),
1401 config: crate::tool_generator::RequestConfig::default(),
1402 };
1403
1404 let mut url_exploded = client
1405 .build_url(&tool_metadata, &extracted_params_exploded)
1406 .unwrap();
1407 HttpClient::add_query_parameters(&mut url_exploded, &extracted_params_exploded.query);
1408 let url_exploded_string = url_exploded.to_string();
1409
1410 let mut query_params_not_exploded = HashMap::new();
1412 query_params_not_exploded.insert(
1413 "include".to_string(),
1414 QueryParameter::new(json!(["asset", "scenes"]), false),
1415 );
1416
1417 let extracted_params_not_exploded = ExtractedParameters {
1418 path: HashMap::new(),
1419 query: query_params_not_exploded,
1420 headers: HashMap::new(),
1421 cookies: HashMap::new(),
1422 body: HashMap::new(),
1423 config: crate::tool_generator::RequestConfig::default(),
1424 };
1425
1426 let mut url_not_exploded = client
1427 .build_url(&tool_metadata, &extracted_params_not_exploded)
1428 .unwrap();
1429 HttpClient::add_query_parameters(
1430 &mut url_not_exploded,
1431 &extracted_params_not_exploded.query,
1432 );
1433 let url_not_exploded_string = url_not_exploded.to_string();
1434
1435 assert!(url_exploded_string.contains("include=asset"));
1437 assert!(url_exploded_string.contains("include=scenes"));
1438
1439 assert!(url_not_exploded_string.contains("include=asset%2Cscenes")); assert_ne!(url_exploded_string, url_not_exploded_string);
1444
1445 println!("Exploded URL: {url_exploded_string}");
1446 println!("Non-exploded URL: {url_not_exploded_string}");
1447 }
1448
1449 #[test]
1450 fn test_is_image_helper() {
1451 let response_png = HttpResponse {
1453 status_code: 200,
1454 status_text: "OK".to_string(),
1455 headers: HashMap::new(),
1456 content_type: Some("image/png".to_string()),
1457 body: String::new(),
1458 body_bytes: None,
1459 is_success: true,
1460 request_method: "GET".to_string(),
1461 request_url: "http://example.com".to_string(),
1462 request_body: String::new(),
1463 };
1464 assert!(response_png.is_image());
1465
1466 let response_jpeg = HttpResponse {
1467 content_type: Some("image/jpeg".to_string()),
1468 ..response_png.clone()
1469 };
1470 assert!(response_jpeg.is_image());
1471
1472 let response_with_charset = HttpResponse {
1474 content_type: Some("image/png; charset=utf-8".to_string()),
1475 ..response_png.clone()
1476 };
1477 assert!(response_with_charset.is_image());
1478
1479 let response_json = HttpResponse {
1481 content_type: Some("application/json".to_string()),
1482 ..response_png.clone()
1483 };
1484 assert!(!response_json.is_image());
1485
1486 let response_text = HttpResponse {
1487 content_type: Some("text/plain".to_string()),
1488 ..response_png.clone()
1489 };
1490 assert!(!response_text.is_image());
1491
1492 let response_no_ct = HttpResponse {
1494 content_type: None,
1495 ..response_png
1496 };
1497 assert!(!response_no_ct.is_image());
1498 }
1499
1500 #[test]
1501 fn test_is_binary_helper() {
1502 let base_response = HttpResponse {
1503 status_code: 200,
1504 status_text: "OK".to_string(),
1505 headers: HashMap::new(),
1506 content_type: None,
1507 body: String::new(),
1508 body_bytes: None,
1509 is_success: true,
1510 request_method: "GET".to_string(),
1511 request_url: "http://example.com".to_string(),
1512 request_body: String::new(),
1513 };
1514
1515 let response_image = HttpResponse {
1517 content_type: Some("image/png".to_string()),
1518 ..base_response.clone()
1519 };
1520 assert!(response_image.is_binary());
1521
1522 let response_audio = HttpResponse {
1524 content_type: Some("audio/mpeg".to_string()),
1525 ..base_response.clone()
1526 };
1527 assert!(response_audio.is_binary());
1528
1529 let response_video = HttpResponse {
1531 content_type: Some("video/mp4".to_string()),
1532 ..base_response.clone()
1533 };
1534 assert!(response_video.is_binary());
1535
1536 let response_json = HttpResponse {
1538 content_type: Some("application/json".to_string()),
1539 ..base_response.clone()
1540 };
1541 assert!(!response_json.is_binary());
1542
1543 assert!(!base_response.is_binary());
1545 }
1546
1547 #[test]
1548 fn test_parse_data_uri_valid_png() {
1549 let uri = "data:image/png;base64,aGVsbG8=";
1551 let result = super::parse_data_uri(uri, "test_field").unwrap();
1552
1553 assert_eq!(result.mime_type, "image/png");
1554 assert_eq!(result.bytes, b"hello");
1555 }
1556
1557 #[test]
1558 fn test_parse_data_uri_valid_jpeg() {
1559 let uri = "data:image/jpeg;base64,d29ybGQ=";
1561 let result = super::parse_data_uri(uri, "image").unwrap();
1562
1563 assert_eq!(result.mime_type, "image/jpeg");
1564 assert_eq!(result.bytes, b"world");
1565 }
1566
1567 #[test]
1568 fn test_parse_data_uri_valid_application_json() {
1569 let uri = "data:application/json;base64,e30=";
1571 let result = super::parse_data_uri(uri, "data").unwrap();
1572
1573 assert_eq!(result.mime_type, "application/json");
1574 assert_eq!(result.bytes, b"{}");
1575 }
1576
1577 #[test]
1578 fn test_parse_data_uri_missing_data_prefix() {
1579 let uri = "image/png;base64,aGVsbG8=";
1580 let result = super::parse_data_uri(uri, "test_field");
1581
1582 assert!(result.is_err());
1583 let err = result.unwrap_err().to_string();
1584 assert!(err.contains("Invalid data URI format"));
1585 assert!(err.contains("test_field"));
1586 assert!(err.contains("expected 'data:<mime>;base64,<content>'"));
1587 }
1588
1589 #[test]
1590 fn test_parse_data_uri_missing_semicolon() {
1591 let uri = "data:image/png,aGVsbG8=";
1592 let result = super::parse_data_uri(uri, "my_image");
1593
1594 assert!(result.is_err());
1595 let err = result.unwrap_err().to_string();
1596 assert!(err.contains("Invalid data URI format"));
1597 assert!(err.contains("my_image"));
1598 }
1599
1600 #[test]
1601 fn test_parse_data_uri_missing_comma() {
1602 let uri = "data:image/png;base64aGVsbG8=";
1603 let result = super::parse_data_uri(uri, "field");
1604
1605 assert!(result.is_err());
1606 let err = result.unwrap_err().to_string();
1607 assert!(err.contains("Invalid data URI format"));
1608 }
1609
1610 #[test]
1611 fn test_parse_data_uri_unsupported_encoding() {
1612 let uri = "data:image/png;ascii,hello";
1613 let result = super::parse_data_uri(uri, "test_field");
1614
1615 assert!(result.is_err());
1616 let err = result.unwrap_err().to_string();
1617 assert!(err.contains("Unsupported encoding 'ascii'"));
1618 assert!(err.contains("test_field"));
1619 assert!(err.contains("only base64 is supported"));
1620 }
1621
1622 #[test]
1623 fn test_parse_data_uri_unsupported_encoding_utf8() {
1624 let uri = "data:text/plain;utf-8,hello world";
1625 let result = super::parse_data_uri(uri, "content");
1626
1627 assert!(result.is_err());
1628 let err = result.unwrap_err().to_string();
1629 assert!(err.contains("Unsupported encoding 'utf-8'"));
1630 assert!(err.contains("content"));
1631 }
1632
1633 #[test]
1634 fn test_parse_data_uri_invalid_base64() {
1635 let uri = "data:image/png;base64,not-valid-base64!!!";
1637 let result = super::parse_data_uri(uri, "bad_image");
1638
1639 assert!(result.is_err());
1640 let err = result.unwrap_err().to_string();
1641 assert!(err.contains("Invalid base64 content"));
1642 assert!(err.contains("bad_image"));
1643 }
1644
1645 #[test]
1646 fn test_parse_data_uri_empty_content() {
1647 let uri = "data:application/octet-stream;base64,";
1649 let result = super::parse_data_uri(uri, "empty").unwrap();
1650
1651 assert_eq!(result.mime_type, "application/octet-stream");
1652 assert!(result.bytes.is_empty());
1653 }
1654
1655 #[test]
1656 fn test_parse_data_uri_complex_mime_type() {
1657 let uri = "data:application/vnd.api+json;base64,e30=";
1659 let result = super::parse_data_uri(uri, "api_data").unwrap();
1660
1661 assert_eq!(result.mime_type, "application/vnd.api+json");
1662 assert_eq!(result.bytes, b"{}");
1663 }
1664
1665 #[test]
1666 fn test_parse_data_uri_mime_type_with_parameters() {
1667 let uri = "data:text/plain;charset=utf-8;base64,SGVsbG8gV29ybGQ=";
1669 let result = super::parse_data_uri(uri, "text_field").unwrap();
1670
1671 assert_eq!(result.mime_type, "text/plain;charset=utf-8");
1672 assert_eq!(result.bytes, b"Hello World");
1673 }
1674
1675 #[test]
1676 fn test_parse_data_uri_mime_type_with_multiple_parameters() {
1677 let uri = "data:text/html;charset=utf-8;boundary=something;base64,PGh0bWw+";
1679 let result = super::parse_data_uri(uri, "html_field").unwrap();
1680
1681 assert_eq!(
1682 result.mime_type,
1683 "text/html;charset=utf-8;boundary=something"
1684 );
1685 assert_eq!(result.bytes, b"<html>");
1686 }
1687
1688 #[test]
1689 fn test_parse_data_uri_empty_mime_type() {
1690 let uri = "data:;base64,SGVsbG8=";
1692 let result = super::parse_data_uri(uri, "field");
1693
1694 assert!(result.is_err());
1695 let err = result.unwrap_err().to_string();
1696 assert!(err.contains("MIME type cannot be empty"));
1697 }
1698
1699 #[test]
1700 fn test_parse_data_uri_empty_string() {
1701 let result = super::parse_data_uri("", "field");
1702
1703 assert!(result.is_err());
1704 let err = result.unwrap_err().to_string();
1705 assert!(err.contains("Invalid data URI format"));
1706 }
1707
1708 #[test]
1709 fn test_parse_data_uri_just_data_prefix() {
1710 let result = super::parse_data_uri("data:", "field");
1711
1712 assert!(result.is_err());
1713 let err = result.unwrap_err().to_string();
1714 assert!(err.contains("Invalid data URI format"));
1715 }
1716
1717 #[test]
1729 fn test_add_request_body_multipart_with_valid_file() {
1730 let client = HttpClient::new();
1731 let request = client.client.post("http://example.com/upload");
1732
1733 let mut body = HashMap::new();
1734 body.insert(
1736 "file".to_string(),
1737 json!({
1738 "content": "data:image/png;base64,iVBORw0KGgo=",
1739 "filename": "test.png"
1740 }),
1741 );
1742 body.insert("description".to_string(), json!("Test file upload"));
1744
1745 let config = crate::tool_generator::RequestConfig {
1746 timeout_seconds: 30,
1747 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1748 };
1749
1750 let result = HttpClient::add_request_body(request, &body, &config);
1751 assert!(
1752 result.is_ok(),
1753 "Should successfully build multipart form with valid file"
1754 );
1755 }
1756
1757 #[test]
1758 fn test_add_request_body_multipart_with_invalid_data_uri() {
1759 let client = HttpClient::new();
1760 let request = client.client.post("http://example.com/upload");
1761
1762 let mut body = HashMap::new();
1763 body.insert(
1765 "file".to_string(),
1766 json!({
1767 "content": "data:image/png,notbase64",
1768 "filename": "test.png"
1769 }),
1770 );
1771
1772 let config = crate::tool_generator::RequestConfig {
1773 timeout_seconds: 30,
1774 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1775 };
1776
1777 let result = HttpClient::add_request_body(request, &body, &config);
1778 assert!(result.is_err(), "Should fail with invalid data URI");
1779 let err = result.unwrap_err().to_string();
1780 assert!(
1781 err.contains("Invalid data URI format"),
1782 "Error should mention invalid format"
1783 );
1784 }
1785
1786 #[test]
1787 fn test_add_request_body_multipart_with_invalid_base64() {
1788 let client = HttpClient::new();
1789 let request = client.client.post("http://example.com/upload");
1790
1791 let mut body = HashMap::new();
1792 body.insert(
1794 "file".to_string(),
1795 json!({
1796 "content": "data:image/png;base64,!!!invalid!!!",
1797 "filename": "test.png"
1798 }),
1799 );
1800
1801 let config = crate::tool_generator::RequestConfig {
1802 timeout_seconds: 30,
1803 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1804 };
1805
1806 let result = HttpClient::add_request_body(request, &body, &config);
1807 assert!(result.is_err(), "Should fail with invalid base64");
1808 let err = result.unwrap_err().to_string();
1809 assert!(
1810 err.contains("Invalid base64 content"),
1811 "Error should mention invalid base64"
1812 );
1813 }
1814
1815 #[test]
1816 fn test_add_request_body_multipart_text_only() {
1817 let client = HttpClient::new();
1818 let request = client.client.post("http://example.com/upload");
1819
1820 let mut body = HashMap::new();
1821 body.insert("field1".to_string(), json!("text value"));
1822 body.insert("field2".to_string(), json!(123));
1823 body.insert("field3".to_string(), json!(true));
1824
1825 let config = crate::tool_generator::RequestConfig {
1826 timeout_seconds: 30,
1827 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1828 };
1829
1830 let result = HttpClient::add_request_body(request, &body, &config);
1831 assert!(
1832 result.is_ok(),
1833 "Should successfully build multipart form with text-only fields"
1834 );
1835 }
1836
1837 #[test]
1838 fn test_add_request_body_multipart_mixed_content() {
1839 let client = HttpClient::new();
1840 let request = client.client.post("http://example.com/upload");
1841
1842 let mut body = HashMap::new();
1843 body.insert(
1845 "image".to_string(),
1846 json!({
1847 "content": "data:image/jpeg;base64,/9j/4AAQ",
1848 "filename": "photo.jpg"
1849 }),
1850 );
1851 body.insert("title".to_string(), json!("My Photo"));
1853 body.insert("tags".to_string(), json!(["nature", "sunset"]));
1854
1855 let config = crate::tool_generator::RequestConfig {
1856 timeout_seconds: 30,
1857 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1858 };
1859
1860 let result = HttpClient::add_request_body(request, &body, &config);
1861 assert!(result.is_ok(), "Should handle mixed file and text content");
1862 }
1863
1864 #[test]
1865 fn test_add_request_body_multipart_without_filename() {
1866 let client = HttpClient::new();
1867 let request = client.client.post("http://example.com/upload");
1868
1869 let mut body = HashMap::new();
1870 body.insert(
1872 "upload".to_string(),
1873 json!({
1874 "content": "data:application/pdf;base64,JVBERi0="
1875 }),
1876 );
1877
1878 let config = crate::tool_generator::RequestConfig {
1879 timeout_seconds: 30,
1880 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1881 };
1882
1883 let result = HttpClient::add_request_body(request, &body, &config);
1884 assert!(
1885 result.is_ok(),
1886 "Should handle file upload without explicit filename"
1887 );
1888 }
1889
1890 #[test]
1891 fn test_add_request_body_json() {
1892 let client = HttpClient::new();
1893 let request = client.client.post("http://example.com/api");
1894
1895 let mut body = HashMap::new();
1896 body.insert("name".to_string(), json!("test"));
1897 body.insert("value".to_string(), json!(42));
1898
1899 let config = crate::tool_generator::RequestConfig {
1900 timeout_seconds: 30,
1901 content_type: mime::APPLICATION_JSON.to_string(),
1902 };
1903
1904 let result = HttpClient::add_request_body(request, &body, &config);
1905 assert!(result.is_ok(), "Should build JSON body");
1906 }
1907
1908 #[test]
1909 fn test_add_request_body_form_urlencoded() {
1910 let client = HttpClient::new();
1911 let request = client.client.post("http://example.com/form");
1912
1913 let mut body = HashMap::new();
1914 body.insert("username".to_string(), json!("user"));
1915 body.insert("password".to_string(), json!("secret"));
1916
1917 let config = crate::tool_generator::RequestConfig {
1918 timeout_seconds: 30,
1919 content_type: mime::APPLICATION_WWW_FORM_URLENCODED.to_string(),
1920 };
1921
1922 let result = HttpClient::add_request_body(request, &body, &config);
1923 assert!(result.is_ok(), "Should build form-urlencoded body");
1924 }
1925
1926 #[tokio::test]
1927 async fn http_client_with_insecure_still_serves_plain_http() {
1928 let mut server = mockito::Server::new_async().await;
1929 let mock = server
1930 .mock("GET", "/ping")
1931 .with_status(200)
1932 .with_body("pong")
1933 .create_async()
1934 .await;
1935
1936 let base_url: Url = server.url().parse().unwrap();
1937 let client = HttpClient::new()
1938 .with_insecure(true)
1939 .with_base_url(base_url.clone())
1940 .unwrap();
1941
1942 let url = base_url.join("ping").unwrap();
1943 let response = client.client.get(url).send().await.unwrap();
1944 assert_eq!(response.status().as_u16(), 200);
1945 assert_eq!(response.text().await.unwrap(), "pong");
1946
1947 mock.assert_async().await;
1948 }
1949
1950 #[test]
1951 fn test_add_request_body_empty() {
1952 let client = HttpClient::new();
1953 let request = client.client.post("http://example.com/api");
1954
1955 let body = HashMap::new();
1956
1957 let config = crate::tool_generator::RequestConfig::default();
1958
1959 let result = HttpClient::add_request_body(request, &body, &config);
1960 assert!(result.is_ok(), "Should handle empty body");
1961 }
1962}