1use rmcp::model::{ErrorCode, ErrorData};
113use schemars::JsonSchema;
114use serde::Serialize;
115use serde_json::{Value, json};
116use std::fmt;
117use thiserror::Error;
118
119#[derive(Debug, Serialize, JsonSchema)]
121#[serde(tag = "type", rename_all = "kebab-case")]
122pub enum ValidationConstraint {
123 Minimum {
125 value: f64,
127 exclusive: bool,
129 },
130 Maximum {
132 value: f64,
134 exclusive: bool,
136 },
137 MinLength {
139 value: usize,
141 },
142 MaxLength {
144 value: usize,
146 },
147 Pattern {
149 pattern: String,
151 },
152 EnumValues {
154 values: Vec<Value>,
156 },
157 Format {
159 format: String,
161 },
162 MultipleOf {
164 value: f64,
166 },
167 MinItems {
169 value: usize,
171 },
172 MaxItems {
174 value: usize,
176 },
177 UniqueItems,
179 MinProperties {
181 value: usize,
183 },
184 MaxProperties {
186 value: usize,
188 },
189 ConstValue {
191 value: Value,
193 },
194 Required {
196 properties: Vec<String>,
198 },
199}
200
201#[derive(Debug, Serialize, JsonSchema)]
203#[serde(tag = "type", rename_all = "kebab-case")]
204pub enum ValidationError {
205 InvalidParameter {
207 parameter: String,
209 suggestions: Vec<String>,
211 valid_parameters: Vec<String>,
213 },
214 MissingRequiredParameter {
216 parameter: String,
218 #[serde(skip_serializing_if = "Option::is_none")]
220 description: Option<String>,
221 expected_type: String,
223 },
224 ConstraintViolation {
226 parameter: String,
228 message: String,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 field_path: Option<String>,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 actual_value: Option<Box<Value>>,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 expected_type: Option<String>,
239 #[serde(skip_serializing_if = "Vec::is_empty")]
241 constraints: Vec<ValidationConstraint>,
242 },
243}
244
245impl fmt::Display for ValidationError {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 match self {
248 ValidationError::InvalidParameter {
249 parameter,
250 suggestions,
251 ..
252 } => {
253 if suggestions.is_empty() {
254 write!(f, "'{parameter}'")
255 } else {
256 write!(f, "'{parameter}' (suggestions: {})", suggestions.join(", "))
257 }
258 }
259 ValidationError::MissingRequiredParameter {
260 parameter,
261 expected_type,
262 ..
263 } => {
264 write!(f, "'{parameter}' is required (expected: {expected_type})")
265 }
266 ValidationError::ConstraintViolation {
267 parameter, message, ..
268 } => {
269 write!(f, "'{parameter}': {message}")
270 }
271 }
272 }
273}
274
275fn format_validation_errors(violations: &[ValidationError]) -> String {
277 match violations.len() {
278 0 => "Validation failed".to_string(),
279 1 => {
280 let error = &violations[0];
282 match error {
283 ValidationError::InvalidParameter { .. } => {
284 format!("Validation failed - invalid parameter {error}")
285 }
286 ValidationError::MissingRequiredParameter { .. } => {
287 format!("Validation failed - missing required parameter: {error}")
288 }
289 ValidationError::ConstraintViolation { .. } => {
290 format!("Validation failed - parameter {error}")
291 }
292 }
293 }
294 _ => {
295 let mut invalid_params = Vec::new();
297 let mut missing_params = Vec::new();
298 let mut constraint_violations = Vec::new();
299
300 for error in violations {
302 match error {
303 ValidationError::InvalidParameter { .. } => {
304 invalid_params.push(error.to_string());
305 }
306 ValidationError::MissingRequiredParameter { .. } => {
307 missing_params.push(error.to_string());
308 }
309 ValidationError::ConstraintViolation { .. } => {
310 constraint_violations.push(error.to_string());
311 }
312 }
313 }
314
315 let mut parts = Vec::new();
316
317 if !invalid_params.is_empty() {
319 let params_str = invalid_params.join(", ");
320 parts.push(format!("invalid parameters: {params_str}"));
321 }
322
323 if !missing_params.is_empty() {
325 let params_str = missing_params.join(", ");
326 parts.push(format!("missing parameters: {params_str}"));
327 }
328
329 if !constraint_violations.is_empty() {
331 let violations_str = constraint_violations.join("; ");
332 parts.push(format!("constraint violations: {violations_str}"));
333 }
334
335 format!("Validation failed - {}", parts.join("; "))
336 }
337 }
338}
339
340#[derive(Debug, Error)]
341pub enum OpenApiError {
342 #[error("Environment variable error: {0}")]
343 EnvVar(#[from] std::env::VarError),
344 #[error("IO error: {0}")]
345 Io(#[from] std::io::Error),
346 #[error("OpenAPI spec error: {0}")]
347 Spec(String),
348 #[error("Tool generation error: {0}")]
349 ToolGeneration(String),
350 #[error("Invalid parameter location: {0}")]
351 InvalidParameterLocation(String),
352 #[error("Invalid URL: {0}")]
353 InvalidUrl(String),
354 #[error("File not found: {0}")]
355 FileNotFound(String),
356 #[error("MCP error: {0}")]
357 McpError(String),
358 #[error("Invalid path: {0}")]
359 InvalidPath(String),
360 #[error("Validation error: {0}")]
361 Validation(String),
362 #[error("HTTP error: {0}")]
363 Http(String),
364 #[error("HTTP request error: {0}")]
365 HttpRequest(#[from] reqwest::Error),
366 #[error("JSON error: {0}")]
367 Json(#[from] serde_json::Error),
368 #[error(transparent)]
369 ToolCall(#[from] ToolCallError),
370}
371
372impl From<ToolCallValidationError> for ErrorData {
373 fn from(err: ToolCallValidationError) -> Self {
374 match err {
375 ToolCallValidationError::ToolNotFound {
376 ref tool_name,
377 ref suggestions,
378 } => {
379 let data = if suggestions.is_empty() {
380 None
381 } else {
382 Some(json!({
383 "suggestions": suggestions
384 }))
385 };
386 ErrorData::new(
387 ErrorCode(-32601),
388 format!("Tool '{tool_name}' not found"),
389 data,
390 )
391 }
392 ToolCallValidationError::InvalidParameters { ref violations } => {
393 let data = Some(json!({
395 "type": "validation-errors",
396 "violations": violations
397 }));
398 ErrorData::new(ErrorCode(-32602), err.to_string(), data)
399 }
400 ToolCallValidationError::RequestConstructionError { ref reason } => {
401 let data = Some(json!({
403 "type": "request-construction-error",
404 "reason": reason
405 }));
406 ErrorData::new(ErrorCode(-32602), err.to_string(), data)
407 }
408 }
409 }
410}
411
412impl From<ToolCallError> for ErrorData {
413 fn from(err: ToolCallError) -> Self {
414 match err {
415 ToolCallError::Validation(validation_err) => validation_err.into(),
416 ToolCallError::Execution(execution_err) => {
417 match execution_err {
421 ToolCallExecutionError::HttpError {
422 status,
423 ref message,
424 ..
425 } => {
426 let data = Some(json!({
427 "type": "http-error",
428 "status": status,
429 "message": message
430 }));
431 ErrorData::new(ErrorCode(-32000), execution_err.to_string(), data)
432 }
433 ToolCallExecutionError::NetworkError {
434 ref message,
435 ref category,
436 } => {
437 let data = Some(json!({
438 "type": "network-error",
439 "message": message,
440 "category": category
441 }));
442 ErrorData::new(ErrorCode(-32000), execution_err.to_string(), data)
443 }
444 ToolCallExecutionError::ResponseParsingError { ref reason, .. } => {
445 let data = Some(json!({
446 "type": "response-parsing-error",
447 "reason": reason
448 }));
449 ErrorData::new(ErrorCode(-32700), execution_err.to_string(), data)
450 }
451 }
452 }
453 }
454 }
455}
456
457impl From<OpenApiError> for ErrorData {
458 fn from(err: OpenApiError) -> Self {
459 match err {
460 OpenApiError::Spec(msg) => ErrorData::new(
461 ErrorCode(-32700),
462 format!("OpenAPI spec error: {msg}"),
463 None,
464 ),
465 OpenApiError::Validation(msg) => {
466 ErrorData::new(ErrorCode(-32602), format!("Validation error: {msg}"), None)
467 }
468 OpenApiError::HttpRequest(e) => {
469 ErrorData::new(ErrorCode(-32000), format!("HTTP request failed: {e}"), None)
470 }
471 OpenApiError::Http(msg) => {
472 ErrorData::new(ErrorCode(-32000), format!("HTTP error: {msg}"), None)
473 }
474 OpenApiError::Json(e) => {
475 ErrorData::new(ErrorCode(-32700), format!("JSON parsing error: {e}"), None)
476 }
477 OpenApiError::ToolCall(e) => e.into(),
478 _ => ErrorData::new(ErrorCode(-32000), err.to_string(), None),
479 }
480 }
481}
482
483#[derive(Debug, Error, Serialize)]
485#[serde(untagged)]
486pub enum ToolCallError {
487 #[error(transparent)]
489 Validation(#[from] ToolCallValidationError),
490
491 #[error(transparent)]
493 Execution(#[from] ToolCallExecutionError),
494}
495
496#[derive(Debug, Serialize, JsonSchema)]
498pub struct ErrorResponse {
499 pub error: ToolCallExecutionError,
501}
502
503#[derive(Debug, Error, Serialize)]
506#[serde(tag = "type", rename_all = "kebab-case")]
507pub enum ToolCallValidationError {
508 #[error("Tool '{tool_name}' not found")]
510 #[serde(rename = "tool-not-found")]
511 ToolNotFound {
512 tool_name: String,
514 suggestions: Vec<String>,
516 },
517
518 #[error("{}", format_validation_errors(violations))]
520 #[serde(rename = "validation-errors")]
521 InvalidParameters {
522 violations: Vec<ValidationError>,
524 },
525
526 #[error("Failed to construct request: {reason}")]
528 #[serde(rename = "request-construction-error")]
529 RequestConstructionError {
530 reason: String,
532 },
533}
534
535#[derive(Debug, Error, Serialize, JsonSchema)]
538#[serde(tag = "type", rename_all = "kebab-case")]
539#[schemars(tag = "type", rename_all = "kebab-case")]
540pub enum ToolCallExecutionError {
541 #[error("HTTP {status} error: {message}")]
543 #[serde(rename = "http-error")]
544 HttpError {
545 status: u16,
547 message: String,
549 #[serde(skip_serializing_if = "Option::is_none")]
551 details: Option<Value>,
552 },
553
554 #[error("Network error: {message}")]
556 #[serde(rename = "network-error")]
557 NetworkError {
558 message: String,
560 category: NetworkErrorCategory,
562 },
563
564 #[error("Failed to parse response: {reason}")]
566 #[serde(rename = "response-parsing-error")]
567 ResponseParsingError {
568 reason: String,
570 #[serde(skip_serializing_if = "Option::is_none")]
572 raw_response: Option<String>,
573 },
574}
575
576#[derive(Debug, Serialize, JsonSchema)]
578#[serde(rename_all = "kebab-case")]
579pub enum NetworkErrorCategory {
580 Timeout,
582 Connect,
584 Request,
586 Body,
588 Decode,
590 Other,
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 use insta::assert_json_snapshot;
598 use serde_json::json;
599
600 #[test]
601 fn test_tool_call_error_serialization_with_details() {
602 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
603 violations: vec![ValidationError::InvalidParameter {
604 parameter: "pet_id".to_string(),
605 suggestions: vec!["petId".to_string()],
606 valid_parameters: vec!["petId".to_string(), "timeout_seconds".to_string()],
607 }],
608 });
609
610 let serialized = serde_json::to_value(&error).unwrap();
611 assert_json_snapshot!(serialized);
612 }
613
614 #[test]
615 fn test_tool_call_error_serialization_without_details() {
616 let error = ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
617 tool_name: "unknownTool".to_string(),
618 suggestions: vec![],
619 });
620
621 let serialized = serde_json::to_value(&error).unwrap();
622 assert_json_snapshot!(serialized);
623 }
624
625 #[test]
626 fn test_tool_call_error_serialization_with_suggestions() {
627 let error = ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
628 tool_name: "getPetByID".to_string(),
629 suggestions: vec!["getPetById".to_string(), "getPetsByStatus".to_string()],
630 });
631
632 let serialized = serde_json::to_value(&error).unwrap();
633 assert_json_snapshot!(serialized);
634 }
635
636 #[test]
637 fn test_tool_call_error_multiple_suggestions() {
638 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
639 violations: vec![ValidationError::InvalidParameter {
640 parameter: "pet_i".to_string(),
641 suggestions: vec!["petId".to_string(), "petInfo".to_string()],
642 valid_parameters: vec![
643 "petId".to_string(),
644 "petInfo".to_string(),
645 "timeout".to_string(),
646 ],
647 }],
648 });
649
650 let serialized = serde_json::to_value(&error).unwrap();
651 assert_json_snapshot!(serialized);
652 }
653
654 #[test]
655 fn test_tool_call_error_no_suggestions() {
656 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
657 violations: vec![ValidationError::InvalidParameter {
658 parameter: "completely_wrong".to_string(),
659 suggestions: vec![],
660 valid_parameters: vec!["petId".to_string(), "timeout".to_string()],
661 }],
662 });
663
664 let serialized = serde_json::to_value(&error).unwrap();
665 assert_json_snapshot!(serialized);
666 }
667
668 #[test]
669 fn test_tool_call_error_validation() {
670 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
671 violations: vec![ValidationError::MissingRequiredParameter {
672 parameter: "field".to_string(),
673 description: Some("Missing required field".to_string()),
674 expected_type: "string".to_string(),
675 }],
676 });
677 let serialized = serde_json::to_value(&error).unwrap();
678 assert_json_snapshot!(serialized);
679 }
680
681 #[test]
682 fn test_tool_call_error_validation_detailed() {
683 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
684 violations: vec![ValidationError::ConstraintViolation {
685 parameter: "age".to_string(),
686 message: "Parameter 'age' must be between 0 and 150".to_string(),
687 field_path: Some("age".to_string()),
688 actual_value: Some(Box::new(json!(200))),
689 expected_type: Some("integer".to_string()),
690 constraints: vec![
691 ValidationConstraint::Minimum {
692 value: 0.0,
693 exclusive: false,
694 },
695 ValidationConstraint::Maximum {
696 value: 150.0,
697 exclusive: false,
698 },
699 ],
700 }],
701 });
702
703 let serialized = serde_json::to_value(&error).unwrap();
704 assert_json_snapshot!(serialized);
705 }
706
707 #[test]
708 fn test_tool_call_error_validation_enum() {
709 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
710 violations: vec![ValidationError::ConstraintViolation {
711 parameter: "status".to_string(),
712 message: "Parameter 'status' must be one of: available, pending, sold".to_string(),
713 field_path: Some("status".to_string()),
714 actual_value: Some(Box::new(json!("unknown"))),
715 expected_type: Some("string".to_string()),
716 constraints: vec![ValidationConstraint::EnumValues {
717 values: vec![json!("available"), json!("pending"), json!("sold")],
718 }],
719 }],
720 });
721
722 let serialized = serde_json::to_value(&error).unwrap();
723 assert_json_snapshot!(serialized);
724 }
725
726 #[test]
727 fn test_tool_call_error_validation_format() {
728 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
729 violations: vec![ValidationError::ConstraintViolation {
730 parameter: "email".to_string(),
731 message: "Invalid email format".to_string(),
732 field_path: Some("contact.email".to_string()),
733 actual_value: Some(Box::new(json!("not-an-email"))),
734 expected_type: Some("string".to_string()),
735 constraints: vec![ValidationConstraint::Format {
736 format: "email".to_string(),
737 }],
738 }],
739 });
740
741 let serialized = serde_json::to_value(&error).unwrap();
742 assert_json_snapshot!(serialized);
743 }
744
745 #[test]
746 fn test_tool_call_error_http_error() {
747 let error = ToolCallError::Execution(ToolCallExecutionError::HttpError {
748 status: 404,
749 message: "Not found".to_string(),
750 details: None,
751 });
752 let serialized = serde_json::to_value(&error).unwrap();
753 assert_json_snapshot!(serialized);
754 }
755
756 #[test]
757 fn test_tool_call_error_http_request() {
758 let error = ToolCallError::Execution(ToolCallExecutionError::NetworkError {
759 message: "Connection timeout".to_string(),
760 category: NetworkErrorCategory::Timeout,
761 });
762 let serialized = serde_json::to_value(&error).unwrap();
763 assert_json_snapshot!(serialized);
764 }
765
766 #[test]
767 fn test_tool_call_error_json() {
768 let error = ToolCallError::Execution(ToolCallExecutionError::ResponseParsingError {
769 reason: "Invalid JSON".to_string(),
770 raw_response: None,
771 });
772 let serialized = serde_json::to_value(&error).unwrap();
773 assert_json_snapshot!(serialized);
774 }
775
776 #[test]
777 fn test_tool_call_error_request_construction() {
778 let error = ToolCallError::Validation(ToolCallValidationError::RequestConstructionError {
779 reason: "Invalid parameter location: body".to_string(),
780 });
781 let serialized = serde_json::to_value(&error).unwrap();
782 assert_json_snapshot!(serialized);
783 }
784
785 #[test]
786 fn test_error_response_serialization() {
787 let error = ToolCallExecutionError::HttpError {
788 status: 400,
789 message: "Bad Request".to_string(),
790 details: Some(json!({
791 "error": "Invalid parameter",
792 "parameter": "test_param"
793 })),
794 };
795
796 let response = ErrorResponse { error };
797 let serialized = serde_json::to_value(&response).unwrap();
798 assert_json_snapshot!(serialized);
799 }
800
801 #[test]
802 fn test_tool_call_error_validation_multiple_of() {
803 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
804 violations: vec![ValidationError::ConstraintViolation {
805 parameter: "price".to_string(),
806 message: "10.5 is not a multiple of 3".to_string(),
807 field_path: Some("price".to_string()),
808 actual_value: Some(Box::new(json!(10.5))),
809 expected_type: Some("number".to_string()),
810 constraints: vec![ValidationConstraint::MultipleOf { value: 3.0 }],
811 }],
812 });
813
814 let serialized = serde_json::to_value(&error).unwrap();
815 assert_json_snapshot!(serialized);
816 }
817
818 #[test]
819 fn test_tool_call_error_validation_min_items() {
820 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
821 violations: vec![ValidationError::ConstraintViolation {
822 parameter: "tags".to_string(),
823 message: "Array has 1 items but minimum is 2".to_string(),
824 field_path: Some("tags".to_string()),
825 actual_value: Some(Box::new(json!(["tag1"]))),
826 expected_type: Some("array".to_string()),
827 constraints: vec![ValidationConstraint::MinItems { value: 2 }],
828 }],
829 });
830
831 let serialized = serde_json::to_value(&error).unwrap();
832 assert_json_snapshot!(serialized);
833 }
834
835 #[test]
836 fn test_tool_call_error_validation_max_items() {
837 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
838 violations: vec![ValidationError::ConstraintViolation {
839 parameter: "categories".to_string(),
840 message: "Array has 4 items but maximum is 3".to_string(),
841 field_path: Some("categories".to_string()),
842 actual_value: Some(Box::new(json!(["a", "b", "c", "d"]))),
843 expected_type: Some("array".to_string()),
844 constraints: vec![ValidationConstraint::MaxItems { value: 3 }],
845 }],
846 });
847
848 let serialized = serde_json::to_value(&error).unwrap();
849 assert_json_snapshot!(serialized);
850 }
851
852 #[test]
853 fn test_tool_call_error_validation_unique_items() {
854 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
855 violations: vec![ValidationError::ConstraintViolation {
856 parameter: "numbers".to_string(),
857 message: "Array items [1, 2, 2, 3] are not unique".to_string(),
858 field_path: Some("numbers".to_string()),
859 actual_value: Some(Box::new(json!([1, 2, 2, 3]))),
860 expected_type: Some("array".to_string()),
861 constraints: vec![ValidationConstraint::UniqueItems],
862 }],
863 });
864
865 let serialized = serde_json::to_value(&error).unwrap();
866 assert_json_snapshot!(serialized);
867 }
868
869 #[test]
870 fn test_tool_call_error_validation_min_properties() {
871 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
872 violations: vec![ValidationError::ConstraintViolation {
873 parameter: "metadata".to_string(),
874 message: "Object has 2 properties but minimum is 3".to_string(),
875 field_path: Some("metadata".to_string()),
876 actual_value: Some(Box::new(json!({"name": "test", "version": "1.0"}))),
877 expected_type: Some("object".to_string()),
878 constraints: vec![ValidationConstraint::MinProperties { value: 3 }],
879 }],
880 });
881
882 let serialized = serde_json::to_value(&error).unwrap();
883 assert_json_snapshot!(serialized);
884 }
885
886 #[test]
887 fn test_tool_call_error_validation_max_properties() {
888 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
889 violations: vec![ValidationError::ConstraintViolation {
890 parameter: "config".to_string(),
891 message: "Object has 3 properties but maximum is 2".to_string(),
892 field_path: Some("config".to_string()),
893 actual_value: Some(Box::new(json!({"a": 1, "b": 2, "c": 3}))),
894 expected_type: Some("object".to_string()),
895 constraints: vec![ValidationConstraint::MaxProperties { value: 2 }],
896 }],
897 });
898
899 let serialized = serde_json::to_value(&error).unwrap();
900 assert_json_snapshot!(serialized);
901 }
902
903 #[test]
904 fn test_tool_call_error_validation_const() {
905 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
906 violations: vec![ValidationError::ConstraintViolation {
907 parameter: "environment".to_string(),
908 message: r#""staging" is not equal to const "production""#.to_string(),
909 field_path: Some("environment".to_string()),
910 actual_value: Some(Box::new(json!("staging"))),
911 expected_type: Some("string".to_string()),
912 constraints: vec![ValidationConstraint::ConstValue {
913 value: json!("production"),
914 }],
915 }],
916 });
917
918 let serialized = serde_json::to_value(&error).unwrap();
919 assert_json_snapshot!(serialized);
920 }
921
922 #[test]
923 fn test_error_data_conversion_preserves_details() {
924 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
926 violations: vec![ValidationError::InvalidParameter {
927 parameter: "page".to_string(),
928 suggestions: vec!["page_number".to_string()],
929 valid_parameters: vec!["page_number".to_string(), "page_size".to_string()],
930 }],
931 });
932
933 let error_data: ErrorData = error.into();
934 let error_json = serde_json::to_value(&error_data).unwrap();
935
936 assert!(error_json["data"].is_object(), "Should have data field");
938 assert_eq!(
939 error_json["data"]["type"].as_str(),
940 Some("validation-errors"),
941 "Should have validation-errors type"
942 );
943
944 let network_error = ToolCallError::Execution(ToolCallExecutionError::NetworkError {
946 message: "SSL/TLS connection failed - certificate verification error".to_string(),
947 category: NetworkErrorCategory::Connect,
948 });
949
950 let error_data: ErrorData = network_error.into();
951 let error_json = serde_json::to_value(&error_data).unwrap();
952
953 assert!(error_json["data"].is_object(), "Should have data field");
954 assert_eq!(
955 error_json["data"]["type"].as_str(),
956 Some("network-error"),
957 "Should have network-error type"
958 );
959 assert!(
960 error_json["data"]["message"]
961 .as_str()
962 .unwrap()
963 .contains("SSL/TLS"),
964 "Should preserve error message"
965 );
966 }
967}