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
110#[derive(Clone)]
112pub struct HttpClient {
113 client: Client,
114 base_url: Option<Url>,
115 default_headers: HeaderMap,
116}
117
118impl HttpClient {
119 fn create_user_agent() -> String {
121 format!("rmcp-openapi-server/{}", env!("CARGO_PKG_VERSION"))
122 }
123 #[must_use]
129 pub fn new() -> Self {
130 let user_agent = Self::create_user_agent();
131 let client = Client::builder()
132 .user_agent(&user_agent)
133 .timeout(Duration::from_secs(30))
134 .build()
135 .expect("Failed to create HTTP client");
136
137 Self {
138 client,
139 base_url: None,
140 default_headers: HeaderMap::new(),
141 }
142 }
143
144 #[must_use]
150 pub fn with_timeout(timeout_seconds: u64) -> Self {
151 let user_agent = Self::create_user_agent();
152 let client = Client::builder()
153 .user_agent(&user_agent)
154 .timeout(Duration::from_secs(timeout_seconds))
155 .build()
156 .expect("Failed to create HTTP client");
157
158 Self {
159 client,
160 base_url: None,
161 default_headers: HeaderMap::new(),
162 }
163 }
164
165 pub fn with_base_url(mut self, base_url: Url) -> Result<Self, Error> {
171 let mut base_url = base_url;
173 if !base_url.path().ends_with('/') {
174 base_url.set_path(&format!("{}/", base_url.path()));
175 }
176 self.base_url = Some(base_url);
177 Ok(self)
178 }
179
180 #[must_use]
182 pub fn with_default_headers(mut self, default_headers: HeaderMap) -> Self {
183 self.default_headers = default_headers;
184 self
185 }
186
187 #[must_use]
192 pub fn with_authorization(&self, auth_value: &str) -> Self {
193 let mut headers = self.default_headers.clone();
194 if let Ok(header_value) = HeaderValue::from_str(auth_value) {
195 headers.insert(header::AUTHORIZATION, header_value);
196 }
197
198 Self {
199 client: self.client.clone(),
200 base_url: self.base_url.clone(),
201 default_headers: headers,
202 }
203 }
204
205 pub async fn execute_tool_call(
211 &self,
212 tool_metadata: &ToolMetadata,
213 arguments: &Value,
214 ) -> Result<HttpResponse, ToolCallError> {
215 let span = info_span!(
216 "http_request",
217 operation_id = %tool_metadata.name,
218 method = %tool_metadata.method,
219 path = %tool_metadata.path
220 );
221 let _enter = span.enter();
222
223 debug!(
224 "Executing tool call: {} {} with arguments: {}",
225 tool_metadata.method,
226 tool_metadata.path,
227 serde_json::to_string_pretty(arguments).unwrap_or_else(|_| "invalid json".to_string())
228 );
229
230 let extracted_params = ToolGenerator::extract_parameters(tool_metadata, arguments)?;
232
233 debug!(
234 "Extracted parameters: path={:?}, query={:?}, headers={:?}, cookies={:?}",
235 extracted_params.path,
236 extracted_params.query,
237 extracted_params.headers,
238 extracted_params.cookies
239 );
240
241 let mut url = self
243 .build_url(tool_metadata, &extracted_params)
244 .map_err(|e| {
245 ToolCallError::Validation(ToolCallValidationError::RequestConstructionError {
246 reason: e.to_string(),
247 })
248 })?;
249
250 if !extracted_params.query.is_empty() {
252 Self::add_query_parameters(&mut url, &extracted_params.query);
253 }
254
255 info!("Final URL: {}", url);
256
257 let mut request = self
259 .create_request(&tool_metadata.method, &url)
260 .map_err(|e| {
261 ToolCallError::Validation(ToolCallValidationError::RequestConstructionError {
262 reason: e.to_string(),
263 })
264 })?;
265
266 if !self.default_headers.is_empty() {
268 request = Self::add_headers_from_map(request, &self.default_headers);
270 }
271
272 if !extracted_params.headers.is_empty() {
274 request = Self::add_headers(request, &extracted_params.headers);
275 }
276
277 if !extracted_params.cookies.is_empty() {
279 request = Self::add_cookies(request, &extracted_params.cookies);
280 }
281
282 if !extracted_params.body.is_empty() {
284 request =
285 Self::add_request_body(request, &extracted_params.body, &extracted_params.config)
286 .map_err(|e| {
287 ToolCallError::Execution(ToolCallExecutionError::ResponseParsingError {
288 reason: format!("Failed to serialize request body: {e}"),
289 raw_response: None,
290 })
291 })?;
292 }
293
294 if extracted_params.config.timeout_seconds != 30 {
296 request = request.timeout(Duration::from_secs(u64::from(
297 extracted_params.config.timeout_seconds,
298 )));
299 }
300
301 let request_body_string = if extracted_params.body.is_empty() {
303 String::new()
304 } else if extracted_params.body.len() == 1
305 && extracted_params.body.contains_key("request_body")
306 {
307 serde_json::to_string(&extracted_params.body["request_body"]).unwrap_or_default()
308 } else {
309 let body_object = Value::Object(
310 extracted_params
311 .body
312 .iter()
313 .map(|(k, v)| (k.clone(), v.clone()))
314 .collect(),
315 );
316 serde_json::to_string(&body_object).unwrap_or_default()
317 };
318
319 let final_url = url.to_string();
321
322 debug!("Sending HTTP request...");
324 let start_time = std::time::Instant::now();
325 let response = request.send().await.map_err(|e| {
326 error!(
327 operation_id = %tool_metadata.name,
328 method = %tool_metadata.method,
329 url = %final_url,
330 error = %e,
331 "HTTP request failed"
332 );
333
334 let (error_msg, category) = if e.is_timeout() {
336 (
337 format!(
338 "Request timeout after {} seconds while calling {} {}",
339 extracted_params.config.timeout_seconds,
340 tool_metadata.method.to_uppercase(),
341 final_url
342 ),
343 NetworkErrorCategory::Timeout,
344 )
345 } else if e.is_connect() {
346 (
347 format!(
348 "Connection failed to {final_url} - Error: {e}. Check if the server is running and the URL is correct."
349 ),
350 NetworkErrorCategory::Connect,
351 )
352 } else if e.is_request() {
353 (
354 format!(
355 "Request error while calling {} {} - Error: {}",
356 tool_metadata.method.to_uppercase(),
357 final_url,
358 e
359 ),
360 NetworkErrorCategory::Request,
361 )
362 } else if e.is_body() {
363 (
364 format!(
365 "Body error while calling {} {} - Error: {}",
366 tool_metadata.method.to_uppercase(),
367 final_url,
368 e
369 ),
370 NetworkErrorCategory::Body,
371 )
372 } else if e.is_decode() {
373 (
374 format!(
375 "Response decode error from {} {} - Error: {}",
376 tool_metadata.method.to_uppercase(),
377 final_url,
378 e
379 ),
380 NetworkErrorCategory::Decode,
381 )
382 } else {
383 (
384 format!(
385 "HTTP request failed: {} (URL: {}, Method: {})",
386 e,
387 final_url,
388 tool_metadata.method.to_uppercase()
389 ),
390 NetworkErrorCategory::Other,
391 )
392 };
393
394 ToolCallError::Execution(ToolCallExecutionError::NetworkError {
395 message: error_msg,
396 category,
397 })
398 })?;
399
400 let elapsed = start_time.elapsed();
401 info!(
402 operation_id = %tool_metadata.name,
403 method = %tool_metadata.method,
404 url = %final_url,
405 status = response.status().as_u16(),
406 elapsed_ms = elapsed.as_millis(),
407 "HTTP request completed"
408 );
409 debug!("Response received with status: {}", response.status());
410
411 self.process_response_with_request(
413 response,
414 &tool_metadata.method,
415 &final_url,
416 &request_body_string,
417 )
418 .await
419 .map_err(|e| {
420 ToolCallError::Execution(ToolCallExecutionError::HttpError {
421 status: 0,
422 message: e.to_string(),
423 details: None,
424 })
425 })
426 }
427
428 fn build_url(
430 &self,
431 tool_metadata: &ToolMetadata,
432 extracted_params: &ExtractedParameters,
433 ) -> Result<Url, Error> {
434 let mut path = tool_metadata.path.clone();
435
436 for (param_name, param_value) in &extracted_params.path {
438 let placeholder = format!("{{{param_name}}}");
439 let value_str = match param_value {
440 Value::String(s) => s.clone(),
441 Value::Number(n) => n.to_string(),
442 Value::Bool(b) => b.to_string(),
443 _ => param_value.to_string(),
444 };
445 path = path.replace(&placeholder, &value_str);
446 }
447
448 let mut path: &str = path.as_ref();
449
450 if let Some(base_url) = &self.base_url {
452 if path.starts_with('/') {
455 path = &path[1..];
456 }
457 base_url.join(path).map_err(|e| {
458 Error::Http(format!(
459 "Failed to join URL '{base_url}' with path '{path}': {e}"
460 ))
461 })
462 } else {
463 if path.starts_with("http") {
465 Url::parse(path).map_err(|e| Error::Http(format!("Invalid URL '{path}': {e}")))
466 } else {
467 Err(Error::Http(
468 "No base URL configured and path is not a complete URL".to_string(),
469 ))
470 }
471 }
472 }
473
474 fn create_request(&self, method: &str, url: &Url) -> Result<RequestBuilder, Error> {
476 let http_method = method.to_uppercase();
477 let method = match http_method.as_str() {
478 "GET" => Method::GET,
479 "POST" => Method::POST,
480 "PUT" => Method::PUT,
481 "DELETE" => Method::DELETE,
482 "PATCH" => Method::PATCH,
483 "HEAD" => Method::HEAD,
484 "OPTIONS" => Method::OPTIONS,
485 _ => {
486 return Err(Error::Http(format!(
487 "Unsupported HTTP method: {http_method}"
488 )));
489 }
490 };
491
492 Ok(self.client.request(method, url.clone()))
493 }
494
495 fn add_query_parameters(url: &mut Url, query_params: &HashMap<String, QueryParameter>) {
497 {
498 let mut query_pairs = url.query_pairs_mut();
499 for (key, query_param) in query_params {
500 if let Value::Array(arr) = &query_param.value {
501 if query_param.explode {
502 for item in arr {
504 let item_str = match item {
505 Value::String(s) => s.clone(),
506 Value::Number(n) => n.to_string(),
507 Value::Bool(b) => b.to_string(),
508 _ => item.to_string(),
509 };
510 query_pairs.append_pair(key, &item_str);
511 }
512 } else {
513 let array_values: Vec<String> = arr
515 .iter()
516 .map(|item| match item {
517 Value::String(s) => s.clone(),
518 Value::Number(n) => n.to_string(),
519 Value::Bool(b) => b.to_string(),
520 _ => item.to_string(),
521 })
522 .collect();
523 let comma_separated = array_values.join(",");
524 query_pairs.append_pair(key, &comma_separated);
525 }
526 } else {
527 let value_str = match &query_param.value {
528 Value::String(s) => s.clone(),
529 Value::Number(n) => n.to_string(),
530 Value::Bool(b) => b.to_string(),
531 _ => query_param.value.to_string(),
532 };
533 query_pairs.append_pair(key, &value_str);
534 }
535 }
536 }
537 }
538
539 fn add_headers_from_map(mut request: RequestBuilder, headers: &HeaderMap) -> RequestBuilder {
541 for (key, value) in headers {
542 request = request.header(key, value);
544 }
545 request
546 }
547
548 fn add_headers(
550 mut request: RequestBuilder,
551 headers: &HashMap<String, Value>,
552 ) -> RequestBuilder {
553 for (key, value) in headers {
554 let value_str = match value {
555 Value::String(s) => s.clone(),
556 Value::Number(n) => n.to_string(),
557 Value::Bool(b) => b.to_string(),
558 _ => value.to_string(),
559 };
560 request = request.header(key, value_str);
561 }
562 request
563 }
564
565 fn add_cookies(
567 mut request: RequestBuilder,
568 cookies: &HashMap<String, Value>,
569 ) -> RequestBuilder {
570 if !cookies.is_empty() {
571 let cookie_header = cookies
572 .iter()
573 .map(|(key, value)| {
574 let value_str = match value {
575 Value::String(s) => s.clone(),
576 Value::Number(n) => n.to_string(),
577 Value::Bool(b) => b.to_string(),
578 _ => value.to_string(),
579 };
580 format!("{key}={value_str}")
581 })
582 .collect::<Vec<_>>()
583 .join("; ");
584
585 request = request.header(header::COOKIE, cookie_header);
586 }
587 request
588 }
589
590 fn add_request_body(
592 mut request: RequestBuilder,
593 body: &HashMap<String, Value>,
594 config: &crate::tool_generator::RequestConfig,
595 ) -> Result<RequestBuilder, Error> {
596 if body.is_empty() {
597 return Ok(request);
598 }
599
600 match config.content_type.as_str() {
602 s if s == mime::APPLICATION_JSON.as_ref() => {
603 request = request.header(header::CONTENT_TYPE, &config.content_type);
605
606 if body.len() == 1 && body.contains_key("request_body") {
608 let body_value = &body["request_body"];
610 let json_string = serde_json::to_string(body_value).map_err(|e| {
611 Error::Http(format!("Failed to serialize request body: {e}"))
612 })?;
613 request = request.body(json_string);
614 } else {
615 let body_object =
617 Value::Object(body.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
618 let json_string = serde_json::to_string(&body_object).map_err(|e| {
619 Error::Http(format!("Failed to serialize request body: {e}"))
620 })?;
621 request = request.body(json_string);
622 }
623 }
624 s if s == mime::APPLICATION_WWW_FORM_URLENCODED.as_ref() => {
625 request = request.header(header::CONTENT_TYPE, &config.content_type);
627
628 let form_data: Vec<(String, String)> = body
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 (key.clone(), value_str)
639 })
640 .collect();
641 request = request.form(&form_data);
642 }
643 s if s == mime::MULTIPART_FORM_DATA.as_ref() => {
644 let mut form = reqwest::multipart::Form::new();
646
647 for (key, value) in body {
648 if let Some(obj) = value.as_object()
650 && let Some(content_value) = obj.get("content")
651 && let Some(content_str) = content_value.as_str()
652 && content_str.starts_with("data:")
653 {
654 let data_uri = parse_data_uri(content_str, key)?;
656
657 let filename = obj
659 .get("filename")
660 .and_then(|v| v.as_str())
661 .unwrap_or("file")
662 .to_string();
663
664 let part = reqwest::multipart::Part::bytes(data_uri.bytes)
666 .file_name(filename)
667 .mime_str(&data_uri.mime_type)
668 .map_err(|e| Error::Http(format!("Invalid MIME type: {e}")))?;
669
670 form = form.part(key.clone(), part);
671 continue;
672 }
673
674 let text_value = match value {
676 Value::String(s) => s.clone(),
677 Value::Number(n) => n.to_string(),
678 Value::Bool(b) => b.to_string(),
679 _ => value.to_string(),
680 };
681 form = form.text(key.clone(), text_value);
682 }
683
684 request = request.multipart(form);
685 }
686 _ => {
687 request = request.header(header::CONTENT_TYPE, &config.content_type);
689
690 let body_object =
692 Value::Object(body.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
693 let json_string = serde_json::to_string(&body_object)
694 .map_err(|e| Error::Http(format!("Failed to serialize request body: {e}")))?;
695 request = request.body(json_string);
696 }
697 }
698
699 Ok(request)
700 }
701
702 async fn process_response_with_request(
704 &self,
705 response: reqwest::Response,
706 method: &str,
707 url: &str,
708 request_body: &str,
709 ) -> Result<HttpResponse, Error> {
710 let status = response.status();
711
712 let content_type = response
714 .headers()
715 .get(header::CONTENT_TYPE)
716 .and_then(|v| v.to_str().ok())
717 .map(|s| s.to_string());
718
719 let is_binary_content = content_type
721 .as_ref()
722 .and_then(|ct| ct.parse::<mime::Mime>().ok())
723 .map(|mime_type| matches!(mime_type.type_(), mime::IMAGE | mime::AUDIO | mime::VIDEO))
724 .unwrap_or(false);
725
726 let headers = response
727 .headers()
728 .iter()
729 .map(|(name, value)| {
730 (
731 name.to_string(),
732 value.to_str().unwrap_or("<invalid>").to_string(),
733 )
734 })
735 .collect();
736
737 let (body, body_bytes) = if is_binary_content {
739 let bytes = response
741 .bytes()
742 .await
743 .map_err(|e| Error::Http(format!("Failed to read response body: {e}")))?;
744
745 let body_text = format!(
747 "[Binary content: {} bytes, Content-Type: {}]",
748 bytes.len(),
749 content_type.as_ref().unwrap_or(&"unknown".to_string())
750 );
751
752 (body_text, Some(bytes.to_vec()))
753 } else {
754 let text = response
756 .text()
757 .await
758 .map_err(|e| Error::Http(format!("Failed to read response body: {e}")))?;
759
760 (text, None)
761 };
762
763 let is_success = status.is_success();
764 let status_code = status.as_u16();
765 let status_text = status.canonical_reason().unwrap_or("Unknown").to_string();
766
767 let enhanced_status_text = match status {
769 StatusCode::BAD_REQUEST => {
770 format!("{status_text} - Bad Request: Check request parameters")
771 }
772 StatusCode::UNAUTHORIZED => {
773 format!("{status_text} - Unauthorized: Authentication required")
774 }
775 StatusCode::FORBIDDEN => format!("{status_text} - Forbidden: Access denied"),
776 StatusCode::NOT_FOUND => {
777 format!("{status_text} - Not Found: Endpoint or resource does not exist")
778 }
779 StatusCode::METHOD_NOT_ALLOWED => format!(
780 "{} - Method Not Allowed: {} method not supported",
781 status_text,
782 method.to_uppercase()
783 ),
784 StatusCode::UNPROCESSABLE_ENTITY => {
785 format!("{status_text} - Unprocessable Entity: Request validation failed")
786 }
787 StatusCode::TOO_MANY_REQUESTS => {
788 format!("{status_text} - Too Many Requests: Rate limit exceeded")
789 }
790 StatusCode::INTERNAL_SERVER_ERROR => {
791 format!("{status_text} - Internal Server Error: Server encountered an error")
792 }
793 StatusCode::BAD_GATEWAY => {
794 format!("{status_text} - Bad Gateway: Upstream server error")
795 }
796 StatusCode::SERVICE_UNAVAILABLE => {
797 format!("{status_text} - Service Unavailable: Server temporarily unavailable")
798 }
799 StatusCode::GATEWAY_TIMEOUT => {
800 format!("{status_text} - Gateway Timeout: Upstream server timeout")
801 }
802 _ => status_text,
803 };
804
805 Ok(HttpResponse {
806 status_code,
807 status_text: enhanced_status_text,
808 headers,
809 content_type,
810 body,
811 body_bytes,
812 is_success,
813 request_method: method.to_string(),
814 request_url: url.to_string(),
815 request_body: request_body.to_string(),
816 })
817 }
818}
819
820impl Default for HttpClient {
821 fn default() -> Self {
822 Self::new()
823 }
824}
825
826#[derive(Debug, Clone)]
828pub struct HttpResponse {
829 pub status_code: u16,
830 pub status_text: String,
831 pub headers: HashMap<String, String>,
832 pub content_type: Option<String>,
833 pub body: String,
834 pub body_bytes: Option<Vec<u8>>,
835 pub is_success: bool,
836 pub request_method: String,
837 pub request_url: String,
838 pub request_body: String,
839}
840
841impl HttpResponse {
842 pub fn json(&self) -> Result<Value, Error> {
848 serde_json::from_str(&self.body)
849 .map_err(|e| Error::Http(format!("Failed to parse response as JSON: {e}")))
850 }
851
852 #[must_use]
856 pub fn is_image(&self) -> bool {
857 self.content_type
858 .as_ref()
859 .and_then(|ct| ct.parse::<mime::Mime>().ok())
860 .map(|mime_type| mime_type.type_() == mime::IMAGE)
861 .unwrap_or(false)
862 }
863
864 #[must_use]
868 pub fn is_binary(&self) -> bool {
869 self.content_type
870 .as_ref()
871 .and_then(|ct| ct.parse::<mime::Mime>().ok())
872 .map(|mime_type| matches!(mime_type.type_(), mime::IMAGE | mime::AUDIO | mime::VIDEO))
873 .unwrap_or(false)
874 }
875
876 #[must_use]
878 pub fn to_mcp_content(&self) -> String {
879 let method = if self.request_method.is_empty() {
880 None
881 } else {
882 Some(self.request_method.as_str())
883 };
884 let url = if self.request_url.is_empty() {
885 None
886 } else {
887 Some(self.request_url.as_str())
888 };
889 let body = if self.request_body.is_empty() {
890 None
891 } else {
892 Some(self.request_body.as_str())
893 };
894 self.to_mcp_content_with_request(method, url, body)
895 }
896
897 pub fn to_mcp_content_with_request(
899 &self,
900 method: Option<&str>,
901 url: Option<&str>,
902 request_body: Option<&str>,
903 ) -> String {
904 let mut result = format!(
905 "HTTP {} {}\n\nStatus: {} {}\n",
906 if self.is_success { "✅" } else { "❌" },
907 if self.is_success { "Success" } else { "Error" },
908 self.status_code,
909 self.status_text
910 );
911
912 if let (Some(method), Some(url)) = (method, url) {
914 result.push_str("\nRequest: ");
915 result.push_str(&method.to_uppercase());
916 result.push(' ');
917 result.push_str(url);
918 result.push('\n');
919
920 if let Some(body) = request_body
921 && !body.is_empty()
922 && body != "{}"
923 {
924 result.push_str("\nRequest Body:\n");
925 if let Ok(parsed) = serde_json::from_str::<Value>(body) {
926 if let Ok(pretty) = serde_json::to_string_pretty(&parsed) {
927 result.push_str(&pretty);
928 } else {
929 result.push_str(body);
930 }
931 } else {
932 result.push_str(body);
933 }
934 result.push('\n');
935 }
936 }
937
938 if !self.headers.is_empty() {
940 result.push_str("\nHeaders:\n");
941 for (key, value) in &self.headers {
942 if [
944 header::CONTENT_TYPE.as_str(),
945 header::CONTENT_LENGTH.as_str(),
946 header::LOCATION.as_str(),
947 header::SET_COOKIE.as_str(),
948 ]
949 .iter()
950 .any(|&h| key.to_lowercase().contains(h))
951 {
952 result.push_str(" ");
953 result.push_str(key);
954 result.push_str(": ");
955 result.push_str(value);
956 result.push('\n');
957 }
958 }
959 }
960
961 result.push_str("\nResponse Body:\n");
963 if self.body.is_empty() {
964 result.push_str("(empty)");
965 } else if let Ok(json_value) = self.json() {
966 match serde_json::to_string_pretty(&json_value) {
968 Ok(pretty) => result.push_str(&pretty),
969 Err(_) => result.push_str(&self.body),
970 }
971 } else {
972 if self.body.len() > 2000 {
974 result.push_str(&self.body[..2000]);
975 result.push_str("\n... (");
976 result.push_str(&(self.body.len() - 2000).to_string());
977 result.push_str(" more characters)");
978 } else {
979 result.push_str(&self.body);
980 }
981 }
982
983 result
984 }
985}
986
987#[cfg(test)]
988mod tests {
989 use super::*;
990 use crate::tool_generator::ExtractedParameters;
991 use serde_json::json;
992 use std::collections::HashMap;
993
994 #[test]
995 fn test_with_base_url_validation() {
996 let url = Url::parse("https://api.example.com").unwrap();
998 let client = HttpClient::new().with_base_url(url);
999 assert!(client.is_ok());
1000
1001 let url = Url::parse("http://localhost:8080").unwrap();
1002 let client = HttpClient::new().with_base_url(url);
1003 assert!(client.is_ok());
1004
1005 assert!(Url::parse("not-a-url").is_err());
1007 assert!(Url::parse("").is_err());
1008
1009 let url = Url::parse("ftp://invalid-scheme.com").unwrap();
1011 let client = HttpClient::new().with_base_url(url);
1012 assert!(client.is_ok()); }
1014
1015 #[test]
1016 fn test_build_url_with_base_url() {
1017 let base_url = Url::parse("https://api.example.com").unwrap();
1018 let client = HttpClient::new().with_base_url(base_url).unwrap();
1019
1020 let tool_metadata = crate::ToolMetadata {
1021 name: "test".to_string(),
1022 title: None,
1023 description: Some("test".to_string()),
1024 parameters: json!({}),
1025 output_schema: None,
1026 method: "GET".to_string(),
1027 path: "/pets/{id}".to_string(),
1028 security: None,
1029 parameter_mappings: std::collections::HashMap::new(),
1030 };
1031
1032 let mut path_params = HashMap::new();
1033 path_params.insert("id".to_string(), json!(123));
1034
1035 let extracted_params = ExtractedParameters {
1036 path: path_params,
1037 query: HashMap::new(),
1038 headers: HashMap::new(),
1039 cookies: HashMap::new(),
1040 body: HashMap::new(),
1041 config: crate::tool_generator::RequestConfig::default(),
1042 };
1043
1044 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1045 assert_eq!(url.to_string(), "https://api.example.com/pets/123");
1046 }
1047
1048 #[test]
1049 fn test_build_url_with_base_url_containing_path() {
1050 let test_cases = vec![
1051 "https://api.example.com/api/v4",
1052 "https://api.example.com/api/v4/",
1053 ];
1054
1055 for base_url in test_cases {
1056 let base_url = Url::parse(base_url).unwrap();
1057 let client = HttpClient::new().with_base_url(base_url).unwrap();
1058
1059 let tool_metadata = crate::ToolMetadata {
1060 name: "test".to_string(),
1061 title: None,
1062 description: Some("test".to_string()),
1063 parameters: json!({}),
1064 output_schema: None,
1065 method: "GET".to_string(),
1066 path: "/pets/{id}".to_string(),
1067 security: None,
1068 parameter_mappings: std::collections::HashMap::new(),
1069 };
1070
1071 let mut path_params = HashMap::new();
1072 path_params.insert("id".to_string(), json!(123));
1073
1074 let extracted_params = ExtractedParameters {
1075 path: path_params,
1076 query: HashMap::new(),
1077 headers: HashMap::new(),
1078 cookies: HashMap::new(),
1079 body: HashMap::new(),
1080 config: crate::tool_generator::RequestConfig::default(),
1081 };
1082
1083 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1084 assert_eq!(url.to_string(), "https://api.example.com/api/v4/pets/123");
1085 }
1086 }
1087
1088 #[test]
1089 fn test_build_url_without_base_url() {
1090 let client = HttpClient::new();
1091
1092 let tool_metadata = crate::ToolMetadata {
1093 name: "test".to_string(),
1094 title: None,
1095 description: Some("test".to_string()),
1096 parameters: json!({}),
1097 output_schema: None,
1098 method: "GET".to_string(),
1099 path: "https://api.example.com/pets/123".to_string(),
1100 security: None,
1101 parameter_mappings: std::collections::HashMap::new(),
1102 };
1103
1104 let extracted_params = ExtractedParameters {
1105 path: HashMap::new(),
1106 query: HashMap::new(),
1107 headers: HashMap::new(),
1108 cookies: HashMap::new(),
1109 body: HashMap::new(),
1110 config: crate::tool_generator::RequestConfig::default(),
1111 };
1112
1113 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1114 assert_eq!(url.to_string(), "https://api.example.com/pets/123");
1115
1116 let tool_metadata_relative = 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/123".to_string(),
1125 security: None,
1126 parameter_mappings: std::collections::HashMap::new(),
1127 };
1128
1129 let result = client.build_url(&tool_metadata_relative, &extracted_params);
1130 assert!(result.is_err());
1131 assert!(
1132 result
1133 .unwrap_err()
1134 .to_string()
1135 .contains("No base URL configured")
1136 );
1137 }
1138
1139 #[test]
1140 fn test_query_parameter_encoding_integration() {
1141 let base_url = Url::parse("https://api.example.com").unwrap();
1142 let client = HttpClient::new().with_base_url(base_url).unwrap();
1143
1144 let tool_metadata = crate::ToolMetadata {
1145 name: "test".to_string(),
1146 title: None,
1147 description: Some("test".to_string()),
1148 parameters: json!({}),
1149 output_schema: None,
1150 method: "GET".to_string(),
1151 path: "/search".to_string(),
1152 security: None,
1153 parameter_mappings: std::collections::HashMap::new(),
1154 };
1155
1156 let mut query_params = HashMap::new();
1158 query_params.insert(
1159 "q".to_string(),
1160 QueryParameter::new(json!("hello world"), true),
1161 ); query_params.insert(
1163 "category".to_string(),
1164 QueryParameter::new(json!("pets&dogs"), true),
1165 ); query_params.insert(
1167 "special".to_string(),
1168 QueryParameter::new(json!("foo=bar"), true),
1169 ); query_params.insert(
1171 "unicode".to_string(),
1172 QueryParameter::new(json!("café"), true),
1173 ); query_params.insert(
1175 "percent".to_string(),
1176 QueryParameter::new(json!("100%"), true),
1177 ); let extracted_params = ExtractedParameters {
1180 path: HashMap::new(),
1181 query: query_params,
1182 headers: HashMap::new(),
1183 cookies: HashMap::new(),
1184 body: HashMap::new(),
1185 config: crate::tool_generator::RequestConfig::default(),
1186 };
1187
1188 let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1189 HttpClient::add_query_parameters(&mut url, &extracted_params.query);
1190
1191 let url_string = url.to_string();
1192
1193 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")); }
1201
1202 #[test]
1203 fn test_array_query_parameters() {
1204 let base_url = Url::parse("https://api.example.com").unwrap();
1205 let client = HttpClient::new().with_base_url(base_url).unwrap();
1206
1207 let tool_metadata = crate::ToolMetadata {
1208 name: "test".to_string(),
1209 title: None,
1210 description: Some("test".to_string()),
1211 parameters: json!({}),
1212 output_schema: None,
1213 method: "GET".to_string(),
1214 path: "/search".to_string(),
1215 security: None,
1216 parameter_mappings: std::collections::HashMap::new(),
1217 };
1218
1219 let mut query_params = HashMap::new();
1220 query_params.insert(
1221 "status".to_string(),
1222 QueryParameter::new(json!(["available", "pending"]), true),
1223 );
1224 query_params.insert(
1225 "tags".to_string(),
1226 QueryParameter::new(json!(["red & blue", "fast=car"]), true),
1227 );
1228
1229 let extracted_params = ExtractedParameters {
1230 path: HashMap::new(),
1231 query: query_params,
1232 headers: HashMap::new(),
1233 cookies: HashMap::new(),
1234 body: HashMap::new(),
1235 config: crate::tool_generator::RequestConfig::default(),
1236 };
1237
1238 let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1239 HttpClient::add_query_parameters(&mut url, &extracted_params.query);
1240
1241 let url_string = url.to_string();
1242
1243 assert!(url_string.contains("status=available"));
1245 assert!(url_string.contains("status=pending"));
1246 assert!(url_string.contains("tags=red+%26+blue")); assert!(url_string.contains("tags=fast%3Dcar")); }
1249
1250 #[test]
1251 fn test_path_parameter_substitution() {
1252 let base_url = Url::parse("https://api.example.com").unwrap();
1253 let client = HttpClient::new().with_base_url(base_url).unwrap();
1254
1255 let tool_metadata = crate::ToolMetadata {
1256 name: "test".to_string(),
1257 title: None,
1258 description: Some("test".to_string()),
1259 parameters: json!({}),
1260 output_schema: None,
1261 method: "GET".to_string(),
1262 path: "/users/{userId}/pets/{petId}".to_string(),
1263 security: None,
1264 parameter_mappings: std::collections::HashMap::new(),
1265 };
1266
1267 let mut path_params = HashMap::new();
1268 path_params.insert("userId".to_string(), json!(42));
1269 path_params.insert("petId".to_string(), json!("special-pet-123"));
1270
1271 let extracted_params = ExtractedParameters {
1272 path: path_params,
1273 query: HashMap::new(),
1274 headers: HashMap::new(),
1275 cookies: HashMap::new(),
1276 body: HashMap::new(),
1277 config: crate::tool_generator::RequestConfig::default(),
1278 };
1279
1280 let url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1281 assert_eq!(
1282 url.to_string(),
1283 "https://api.example.com/users/42/pets/special-pet-123"
1284 );
1285 }
1286
1287 #[test]
1288 fn test_url_join_edge_cases() {
1289 let base_url1 = Url::parse("https://api.example.com/").unwrap();
1291 let client1 = HttpClient::new().with_base_url(base_url1).unwrap();
1292
1293 let base_url2 = Url::parse("https://api.example.com").unwrap();
1294 let client2 = HttpClient::new().with_base_url(base_url2).unwrap();
1295
1296 let tool_metadata = crate::ToolMetadata {
1297 name: "test".to_string(),
1298 title: None,
1299 description: Some("test".to_string()),
1300 parameters: json!({}),
1301 output_schema: None,
1302 method: "GET".to_string(),
1303 path: "/pets".to_string(),
1304 security: None,
1305 parameter_mappings: std::collections::HashMap::new(),
1306 };
1307
1308 let extracted_params = ExtractedParameters {
1309 path: HashMap::new(),
1310 query: HashMap::new(),
1311 headers: HashMap::new(),
1312 cookies: HashMap::new(),
1313 body: HashMap::new(),
1314 config: crate::tool_generator::RequestConfig::default(),
1315 };
1316
1317 let url1 = client1
1318 .build_url(&tool_metadata, &extracted_params)
1319 .unwrap();
1320 let url2 = client2
1321 .build_url(&tool_metadata, &extracted_params)
1322 .unwrap();
1323
1324 assert_eq!(url1.to_string(), "https://api.example.com/pets");
1326 assert_eq!(url2.to_string(), "https://api.example.com/pets");
1327 }
1328
1329 #[test]
1330 fn test_explode_array_parameters() {
1331 let base_url = Url::parse("https://api.example.com").unwrap();
1332 let client = HttpClient::new().with_base_url(base_url).unwrap();
1333
1334 let tool_metadata = crate::ToolMetadata {
1335 name: "test".to_string(),
1336 title: None,
1337 description: Some("test".to_string()),
1338 parameters: json!({}),
1339 output_schema: None,
1340 method: "GET".to_string(),
1341 path: "/search".to_string(),
1342 security: None,
1343 parameter_mappings: std::collections::HashMap::new(),
1344 };
1345
1346 let mut query_params_exploded = HashMap::new();
1348 query_params_exploded.insert(
1349 "include".to_string(),
1350 QueryParameter::new(json!(["asset", "scenes"]), true),
1351 );
1352
1353 let extracted_params_exploded = ExtractedParameters {
1354 path: HashMap::new(),
1355 query: query_params_exploded,
1356 headers: HashMap::new(),
1357 cookies: HashMap::new(),
1358 body: HashMap::new(),
1359 config: crate::tool_generator::RequestConfig::default(),
1360 };
1361
1362 let mut url_exploded = client
1363 .build_url(&tool_metadata, &extracted_params_exploded)
1364 .unwrap();
1365 HttpClient::add_query_parameters(&mut url_exploded, &extracted_params_exploded.query);
1366 let url_exploded_string = url_exploded.to_string();
1367
1368 let mut query_params_not_exploded = HashMap::new();
1370 query_params_not_exploded.insert(
1371 "include".to_string(),
1372 QueryParameter::new(json!(["asset", "scenes"]), false),
1373 );
1374
1375 let extracted_params_not_exploded = ExtractedParameters {
1376 path: HashMap::new(),
1377 query: query_params_not_exploded,
1378 headers: HashMap::new(),
1379 cookies: HashMap::new(),
1380 body: HashMap::new(),
1381 config: crate::tool_generator::RequestConfig::default(),
1382 };
1383
1384 let mut url_not_exploded = client
1385 .build_url(&tool_metadata, &extracted_params_not_exploded)
1386 .unwrap();
1387 HttpClient::add_query_parameters(
1388 &mut url_not_exploded,
1389 &extracted_params_not_exploded.query,
1390 );
1391 let url_not_exploded_string = url_not_exploded.to_string();
1392
1393 assert!(url_exploded_string.contains("include=asset"));
1395 assert!(url_exploded_string.contains("include=scenes"));
1396
1397 assert!(url_not_exploded_string.contains("include=asset%2Cscenes")); assert_ne!(url_exploded_string, url_not_exploded_string);
1402
1403 println!("Exploded URL: {url_exploded_string}");
1404 println!("Non-exploded URL: {url_not_exploded_string}");
1405 }
1406
1407 #[test]
1408 fn test_is_image_helper() {
1409 let response_png = HttpResponse {
1411 status_code: 200,
1412 status_text: "OK".to_string(),
1413 headers: HashMap::new(),
1414 content_type: Some("image/png".to_string()),
1415 body: String::new(),
1416 body_bytes: None,
1417 is_success: true,
1418 request_method: "GET".to_string(),
1419 request_url: "http://example.com".to_string(),
1420 request_body: String::new(),
1421 };
1422 assert!(response_png.is_image());
1423
1424 let response_jpeg = HttpResponse {
1425 content_type: Some("image/jpeg".to_string()),
1426 ..response_png.clone()
1427 };
1428 assert!(response_jpeg.is_image());
1429
1430 let response_with_charset = HttpResponse {
1432 content_type: Some("image/png; charset=utf-8".to_string()),
1433 ..response_png.clone()
1434 };
1435 assert!(response_with_charset.is_image());
1436
1437 let response_json = HttpResponse {
1439 content_type: Some("application/json".to_string()),
1440 ..response_png.clone()
1441 };
1442 assert!(!response_json.is_image());
1443
1444 let response_text = HttpResponse {
1445 content_type: Some("text/plain".to_string()),
1446 ..response_png.clone()
1447 };
1448 assert!(!response_text.is_image());
1449
1450 let response_no_ct = HttpResponse {
1452 content_type: None,
1453 ..response_png
1454 };
1455 assert!(!response_no_ct.is_image());
1456 }
1457
1458 #[test]
1459 fn test_is_binary_helper() {
1460 let base_response = HttpResponse {
1461 status_code: 200,
1462 status_text: "OK".to_string(),
1463 headers: HashMap::new(),
1464 content_type: None,
1465 body: String::new(),
1466 body_bytes: None,
1467 is_success: true,
1468 request_method: "GET".to_string(),
1469 request_url: "http://example.com".to_string(),
1470 request_body: String::new(),
1471 };
1472
1473 let response_image = HttpResponse {
1475 content_type: Some("image/png".to_string()),
1476 ..base_response.clone()
1477 };
1478 assert!(response_image.is_binary());
1479
1480 let response_audio = HttpResponse {
1482 content_type: Some("audio/mpeg".to_string()),
1483 ..base_response.clone()
1484 };
1485 assert!(response_audio.is_binary());
1486
1487 let response_video = HttpResponse {
1489 content_type: Some("video/mp4".to_string()),
1490 ..base_response.clone()
1491 };
1492 assert!(response_video.is_binary());
1493
1494 let response_json = HttpResponse {
1496 content_type: Some("application/json".to_string()),
1497 ..base_response.clone()
1498 };
1499 assert!(!response_json.is_binary());
1500
1501 assert!(!base_response.is_binary());
1503 }
1504
1505 #[test]
1506 fn test_parse_data_uri_valid_png() {
1507 let uri = "data:image/png;base64,aGVsbG8=";
1509 let result = super::parse_data_uri(uri, "test_field").unwrap();
1510
1511 assert_eq!(result.mime_type, "image/png");
1512 assert_eq!(result.bytes, b"hello");
1513 }
1514
1515 #[test]
1516 fn test_parse_data_uri_valid_jpeg() {
1517 let uri = "data:image/jpeg;base64,d29ybGQ=";
1519 let result = super::parse_data_uri(uri, "image").unwrap();
1520
1521 assert_eq!(result.mime_type, "image/jpeg");
1522 assert_eq!(result.bytes, b"world");
1523 }
1524
1525 #[test]
1526 fn test_parse_data_uri_valid_application_json() {
1527 let uri = "data:application/json;base64,e30=";
1529 let result = super::parse_data_uri(uri, "data").unwrap();
1530
1531 assert_eq!(result.mime_type, "application/json");
1532 assert_eq!(result.bytes, b"{}");
1533 }
1534
1535 #[test]
1536 fn test_parse_data_uri_missing_data_prefix() {
1537 let uri = "image/png;base64,aGVsbG8=";
1538 let result = super::parse_data_uri(uri, "test_field");
1539
1540 assert!(result.is_err());
1541 let err = result.unwrap_err().to_string();
1542 assert!(err.contains("Invalid data URI format"));
1543 assert!(err.contains("test_field"));
1544 assert!(err.contains("expected 'data:<mime>;base64,<content>'"));
1545 }
1546
1547 #[test]
1548 fn test_parse_data_uri_missing_semicolon() {
1549 let uri = "data:image/png,aGVsbG8=";
1550 let result = super::parse_data_uri(uri, "my_image");
1551
1552 assert!(result.is_err());
1553 let err = result.unwrap_err().to_string();
1554 assert!(err.contains("Invalid data URI format"));
1555 assert!(err.contains("my_image"));
1556 }
1557
1558 #[test]
1559 fn test_parse_data_uri_missing_comma() {
1560 let uri = "data:image/png;base64aGVsbG8=";
1561 let result = super::parse_data_uri(uri, "field");
1562
1563 assert!(result.is_err());
1564 let err = result.unwrap_err().to_string();
1565 assert!(err.contains("Invalid data URI format"));
1566 }
1567
1568 #[test]
1569 fn test_parse_data_uri_unsupported_encoding() {
1570 let uri = "data:image/png;ascii,hello";
1571 let result = super::parse_data_uri(uri, "test_field");
1572
1573 assert!(result.is_err());
1574 let err = result.unwrap_err().to_string();
1575 assert!(err.contains("Unsupported encoding 'ascii'"));
1576 assert!(err.contains("test_field"));
1577 assert!(err.contains("only base64 is supported"));
1578 }
1579
1580 #[test]
1581 fn test_parse_data_uri_unsupported_encoding_utf8() {
1582 let uri = "data:text/plain;utf-8,hello world";
1583 let result = super::parse_data_uri(uri, "content");
1584
1585 assert!(result.is_err());
1586 let err = result.unwrap_err().to_string();
1587 assert!(err.contains("Unsupported encoding 'utf-8'"));
1588 assert!(err.contains("content"));
1589 }
1590
1591 #[test]
1592 fn test_parse_data_uri_invalid_base64() {
1593 let uri = "data:image/png;base64,not-valid-base64!!!";
1595 let result = super::parse_data_uri(uri, "bad_image");
1596
1597 assert!(result.is_err());
1598 let err = result.unwrap_err().to_string();
1599 assert!(err.contains("Invalid base64 content"));
1600 assert!(err.contains("bad_image"));
1601 }
1602
1603 #[test]
1604 fn test_parse_data_uri_empty_content() {
1605 let uri = "data:application/octet-stream;base64,";
1607 let result = super::parse_data_uri(uri, "empty").unwrap();
1608
1609 assert_eq!(result.mime_type, "application/octet-stream");
1610 assert!(result.bytes.is_empty());
1611 }
1612
1613 #[test]
1614 fn test_parse_data_uri_complex_mime_type() {
1615 let uri = "data:application/vnd.api+json;base64,e30=";
1617 let result = super::parse_data_uri(uri, "api_data").unwrap();
1618
1619 assert_eq!(result.mime_type, "application/vnd.api+json");
1620 assert_eq!(result.bytes, b"{}");
1621 }
1622
1623 #[test]
1624 fn test_parse_data_uri_mime_type_with_parameters() {
1625 let uri = "data:text/plain;charset=utf-8;base64,SGVsbG8gV29ybGQ=";
1627 let result = super::parse_data_uri(uri, "text_field").unwrap();
1628
1629 assert_eq!(result.mime_type, "text/plain;charset=utf-8");
1630 assert_eq!(result.bytes, b"Hello World");
1631 }
1632
1633 #[test]
1634 fn test_parse_data_uri_mime_type_with_multiple_parameters() {
1635 let uri = "data:text/html;charset=utf-8;boundary=something;base64,PGh0bWw+";
1637 let result = super::parse_data_uri(uri, "html_field").unwrap();
1638
1639 assert_eq!(
1640 result.mime_type,
1641 "text/html;charset=utf-8;boundary=something"
1642 );
1643 assert_eq!(result.bytes, b"<html>");
1644 }
1645
1646 #[test]
1647 fn test_parse_data_uri_empty_mime_type() {
1648 let uri = "data:;base64,SGVsbG8=";
1650 let result = super::parse_data_uri(uri, "field");
1651
1652 assert!(result.is_err());
1653 let err = result.unwrap_err().to_string();
1654 assert!(err.contains("MIME type cannot be empty"));
1655 }
1656
1657 #[test]
1658 fn test_parse_data_uri_empty_string() {
1659 let result = super::parse_data_uri("", "field");
1660
1661 assert!(result.is_err());
1662 let err = result.unwrap_err().to_string();
1663 assert!(err.contains("Invalid data URI format"));
1664 }
1665
1666 #[test]
1667 fn test_parse_data_uri_just_data_prefix() {
1668 let result = super::parse_data_uri("data:", "field");
1669
1670 assert!(result.is_err());
1671 let err = result.unwrap_err().to_string();
1672 assert!(err.contains("Invalid data URI format"));
1673 }
1674
1675 #[test]
1687 fn test_add_request_body_multipart_with_valid_file() {
1688 let client = HttpClient::new();
1689 let request = client.client.post("http://example.com/upload");
1690
1691 let mut body = HashMap::new();
1692 body.insert(
1694 "file".to_string(),
1695 json!({
1696 "content": "data:image/png;base64,iVBORw0KGgo=",
1697 "filename": "test.png"
1698 }),
1699 );
1700 body.insert("description".to_string(), json!("Test file upload"));
1702
1703 let config = crate::tool_generator::RequestConfig {
1704 timeout_seconds: 30,
1705 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1706 };
1707
1708 let result = HttpClient::add_request_body(request, &body, &config);
1709 assert!(
1710 result.is_ok(),
1711 "Should successfully build multipart form with valid file"
1712 );
1713 }
1714
1715 #[test]
1716 fn test_add_request_body_multipart_with_invalid_data_uri() {
1717 let client = HttpClient::new();
1718 let request = client.client.post("http://example.com/upload");
1719
1720 let mut body = HashMap::new();
1721 body.insert(
1723 "file".to_string(),
1724 json!({
1725 "content": "data:image/png,notbase64",
1726 "filename": "test.png"
1727 }),
1728 );
1729
1730 let config = crate::tool_generator::RequestConfig {
1731 timeout_seconds: 30,
1732 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1733 };
1734
1735 let result = HttpClient::add_request_body(request, &body, &config);
1736 assert!(result.is_err(), "Should fail with invalid data URI");
1737 let err = result.unwrap_err().to_string();
1738 assert!(
1739 err.contains("Invalid data URI format"),
1740 "Error should mention invalid format"
1741 );
1742 }
1743
1744 #[test]
1745 fn test_add_request_body_multipart_with_invalid_base64() {
1746 let client = HttpClient::new();
1747 let request = client.client.post("http://example.com/upload");
1748
1749 let mut body = HashMap::new();
1750 body.insert(
1752 "file".to_string(),
1753 json!({
1754 "content": "data:image/png;base64,!!!invalid!!!",
1755 "filename": "test.png"
1756 }),
1757 );
1758
1759 let config = crate::tool_generator::RequestConfig {
1760 timeout_seconds: 30,
1761 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1762 };
1763
1764 let result = HttpClient::add_request_body(request, &body, &config);
1765 assert!(result.is_err(), "Should fail with invalid base64");
1766 let err = result.unwrap_err().to_string();
1767 assert!(
1768 err.contains("Invalid base64 content"),
1769 "Error should mention invalid base64"
1770 );
1771 }
1772
1773 #[test]
1774 fn test_add_request_body_multipart_text_only() {
1775 let client = HttpClient::new();
1776 let request = client.client.post("http://example.com/upload");
1777
1778 let mut body = HashMap::new();
1779 body.insert("field1".to_string(), json!("text value"));
1780 body.insert("field2".to_string(), json!(123));
1781 body.insert("field3".to_string(), json!(true));
1782
1783 let config = crate::tool_generator::RequestConfig {
1784 timeout_seconds: 30,
1785 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1786 };
1787
1788 let result = HttpClient::add_request_body(request, &body, &config);
1789 assert!(
1790 result.is_ok(),
1791 "Should successfully build multipart form with text-only fields"
1792 );
1793 }
1794
1795 #[test]
1796 fn test_add_request_body_multipart_mixed_content() {
1797 let client = HttpClient::new();
1798 let request = client.client.post("http://example.com/upload");
1799
1800 let mut body = HashMap::new();
1801 body.insert(
1803 "image".to_string(),
1804 json!({
1805 "content": "data:image/jpeg;base64,/9j/4AAQ",
1806 "filename": "photo.jpg"
1807 }),
1808 );
1809 body.insert("title".to_string(), json!("My Photo"));
1811 body.insert("tags".to_string(), json!(["nature", "sunset"]));
1812
1813 let config = crate::tool_generator::RequestConfig {
1814 timeout_seconds: 30,
1815 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1816 };
1817
1818 let result = HttpClient::add_request_body(request, &body, &config);
1819 assert!(result.is_ok(), "Should handle mixed file and text content");
1820 }
1821
1822 #[test]
1823 fn test_add_request_body_multipart_without_filename() {
1824 let client = HttpClient::new();
1825 let request = client.client.post("http://example.com/upload");
1826
1827 let mut body = HashMap::new();
1828 body.insert(
1830 "upload".to_string(),
1831 json!({
1832 "content": "data:application/pdf;base64,JVBERi0="
1833 }),
1834 );
1835
1836 let config = crate::tool_generator::RequestConfig {
1837 timeout_seconds: 30,
1838 content_type: mime::MULTIPART_FORM_DATA.to_string(),
1839 };
1840
1841 let result = HttpClient::add_request_body(request, &body, &config);
1842 assert!(
1843 result.is_ok(),
1844 "Should handle file upload without explicit filename"
1845 );
1846 }
1847
1848 #[test]
1849 fn test_add_request_body_json() {
1850 let client = HttpClient::new();
1851 let request = client.client.post("http://example.com/api");
1852
1853 let mut body = HashMap::new();
1854 body.insert("name".to_string(), json!("test"));
1855 body.insert("value".to_string(), json!(42));
1856
1857 let config = crate::tool_generator::RequestConfig {
1858 timeout_seconds: 30,
1859 content_type: mime::APPLICATION_JSON.to_string(),
1860 };
1861
1862 let result = HttpClient::add_request_body(request, &body, &config);
1863 assert!(result.is_ok(), "Should build JSON body");
1864 }
1865
1866 #[test]
1867 fn test_add_request_body_form_urlencoded() {
1868 let client = HttpClient::new();
1869 let request = client.client.post("http://example.com/form");
1870
1871 let mut body = HashMap::new();
1872 body.insert("username".to_string(), json!("user"));
1873 body.insert("password".to_string(), json!("secret"));
1874
1875 let config = crate::tool_generator::RequestConfig {
1876 timeout_seconds: 30,
1877 content_type: mime::APPLICATION_WWW_FORM_URLENCODED.to_string(),
1878 };
1879
1880 let result = HttpClient::add_request_body(request, &body, &config);
1881 assert!(result.is_ok(), "Should build form-urlencoded body");
1882 }
1883
1884 #[test]
1885 fn test_add_request_body_empty() {
1886 let client = HttpClient::new();
1887 let request = client.client.post("http://example.com/api");
1888
1889 let body = HashMap::new();
1890
1891 let config = crate::tool_generator::RequestConfig::default();
1892
1893 let result = HttpClient::add_request_body(request, &body, &config);
1894 assert!(result.is_ok(), "Should handle empty body");
1895 }
1896}