use thiserror::Error;
pub type ProxyResult<T> = std::result::Result<T, ProxyError>;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ProxyError {
#[error("Protocol error: {0}")]
Protocol(#[from] Box<turbomcp_protocol::Error>),
#[error("Transport error: {0}")]
Transport(#[from] turbomcp_transport::TransportError),
#[error("Introspection error: {message}")]
Introspection {
message: String,
context: Option<String>,
},
#[error("Code generation error: {message}")]
Codegen {
message: String,
template: Option<String>,
},
#[error("Configuration error: {message}")]
Configuration {
message: String,
key: Option<String>,
},
#[error("Backend connection error: {message}")]
BackendConnection {
message: String,
backend_type: Option<String>,
},
#[error("Backend error: {message}")]
Backend {
message: String,
operation: Option<String>,
},
#[error("Schema validation error: {message}")]
SchemaValidation {
message: String,
schema_path: Option<String>,
},
#[error("Timeout: {operation} exceeded {timeout_ms}ms")]
Timeout {
operation: String,
timeout_ms: u64,
},
#[error("Rate limit exceeded: {message}")]
RateLimitExceeded {
message: String,
retry_after_ms: Option<u64>,
},
#[cfg(feature = "auth")]
#[error("Authentication error: {0}")]
Auth(String),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[cfg(feature = "runtime")]
#[error("HTTP error: {message}")]
Http {
message: String,
status_code: Option<u16>,
},
}
impl ProxyError {
pub fn introspection(message: impl Into<String>) -> Self {
Self::Introspection {
message: message.into(),
context: None,
}
}
pub fn introspection_with_context(
message: impl Into<String>,
context: impl Into<String>,
) -> Self {
Self::Introspection {
message: message.into(),
context: Some(context.into()),
}
}
pub fn codegen(message: impl Into<String>) -> Self {
Self::Codegen {
message: message.into(),
template: None,
}
}
pub fn codegen_with_template(message: impl Into<String>, template: impl Into<String>) -> Self {
Self::Codegen {
message: message.into(),
template: Some(template.into()),
}
}
pub fn configuration(message: impl Into<String>) -> Self {
Self::Configuration {
message: message.into(),
key: None,
}
}
pub fn configuration_with_key(message: impl Into<String>, key: impl Into<String>) -> Self {
Self::Configuration {
message: message.into(),
key: Some(key.into()),
}
}
pub fn backend_connection(message: impl Into<String>) -> Self {
Self::BackendConnection {
message: message.into(),
backend_type: None,
}
}
pub fn backend_connection_with_type(
message: impl Into<String>,
backend_type: impl Into<String>,
) -> Self {
Self::BackendConnection {
message: message.into(),
backend_type: Some(backend_type.into()),
}
}
pub fn backend(message: impl Into<String>) -> Self {
Self::Backend {
message: message.into(),
operation: None,
}
}
pub fn backend_with_operation(
message: impl Into<String>,
operation: impl Into<String>,
) -> Self {
Self::Backend {
message: message.into(),
operation: Some(operation.into()),
}
}
pub fn schema_validation(message: impl Into<String>) -> Self {
Self::SchemaValidation {
message: message.into(),
schema_path: None,
}
}
pub fn timeout(operation: impl Into<String>, timeout_ms: u64) -> Self {
Self::Timeout {
operation: operation.into(),
timeout_ms,
}
}
pub fn rate_limit_exceeded(message: impl Into<String>) -> Self {
Self::RateLimitExceeded {
message: message.into(),
retry_after_ms: None,
}
}
#[cfg(feature = "runtime")]
pub fn http(message: impl Into<String>) -> Self {
Self::Http {
message: message.into(),
status_code: None,
}
}
#[cfg(feature = "runtime")]
pub fn http_with_status(message: impl Into<String>, status_code: u16) -> Self {
Self::Http {
message: message.into(),
status_code: Some(status_code),
}
}
#[must_use]
pub fn sanitize(&self) -> String {
match self {
Self::Protocol(_) => "Protocol error occurred".to_string(),
Self::Transport(_) => "Transport error occurred".to_string(),
Self::Introspection { .. } => "Server introspection failed".to_string(),
Self::Codegen { .. } => "Code generation failed".to_string(),
Self::Configuration { .. } => "Configuration error".to_string(),
Self::BackendConnection { .. } => "Backend connection failed".to_string(),
Self::Backend { .. } => "Backend operation failed".to_string(),
Self::SchemaValidation { .. } => "Schema validation failed".to_string(),
Self::Timeout { operation, .. } => {
format!("Operation '{operation}' timed out")
}
Self::RateLimitExceeded { .. } => "Rate limit exceeded".to_string(),
#[cfg(feature = "auth")]
Self::Auth(_) => "Authentication error".to_string(),
Self::Serialization(_) => "Data serialization error".to_string(),
Self::Io(_) => "IO error occurred".to_string(),
#[cfg(feature = "runtime")]
Self::Http { status_code, .. } => {
if let Some(code) = status_code {
format!("HTTP error {code}")
} else {
"HTTP error occurred".to_string()
}
}
}
}
#[must_use]
pub fn is_protocol_error(&self) -> bool {
matches!(self, Self::Protocol(_))
}
#[must_use]
pub fn is_transport_error(&self) -> bool {
matches!(self, Self::Transport(_))
}
#[must_use]
pub fn is_retryable(&self) -> bool {
matches!(
self,
Self::Transport(_)
| Self::BackendConnection { .. }
| Self::Timeout { .. }
| Self::Io(_)
)
}
}
pub trait ProxyErrorExt<T> {
fn introspection_context(self, context: impl Into<String>) -> ProxyResult<T>;
fn backend_context(self, context: impl Into<String>) -> ProxyResult<T>;
fn config_context(self, context: impl Into<String>) -> ProxyResult<T>;
}
impl<T, E> ProxyErrorExt<T> for Result<T, E>
where
E: std::error::Error + Send + Sync + 'static,
{
fn introspection_context(self, context: impl Into<String>) -> ProxyResult<T> {
self.map_err(|e| ProxyError::introspection_with_context(e.to_string(), context.into()))
}
fn backend_context(self, context: impl Into<String>) -> ProxyResult<T> {
self.map_err(|e| ProxyError::backend_with_operation(e.to_string(), context.into()))
}
fn config_context(self, context: impl Into<String>) -> ProxyResult<T> {
self.map_err(|e| ProxyError::configuration_with_key(e.to_string(), context.into()))
}
}
impl From<turbomcp_protocol::Error> for ProxyError {
fn from(err: turbomcp_protocol::Error) -> Self {
Self::Protocol(Box::new(err))
}
}
impl From<ProxyError> for turbomcp_protocol::McpError {
fn from(err: ProxyError) -> Self {
match err {
ProxyError::Protocol(protocol_err) => *protocol_err,
ProxyError::Transport(transport_err) => {
turbomcp_protocol::McpError::transport(transport_err.to_string())
}
ProxyError::Introspection { message, context } => {
let msg = if let Some(ctx) = context {
format!("{message}: {ctx}")
} else {
message
};
turbomcp_protocol::McpError::internal(msg)
}
ProxyError::Codegen { message, template } => {
let msg = if let Some(tmpl) = template {
format!("{message} (template: {tmpl})")
} else {
message
};
turbomcp_protocol::McpError::internal(msg)
}
ProxyError::Configuration { message, key } => {
let msg = if let Some(k) = key {
format!("{message} (key: {k})")
} else {
message
};
turbomcp_protocol::McpError::invalid_params(msg)
}
ProxyError::BackendConnection {
message,
backend_type,
} => {
let msg = if let Some(bt) = backend_type {
format!("{message} (backend: {bt})")
} else {
message
};
turbomcp_protocol::McpError::transport(msg)
}
ProxyError::Backend { message, operation } => {
let msg = if let Some(op) = operation {
format!("{message} (operation: {op})")
} else {
message
};
turbomcp_protocol::McpError::internal(msg)
}
ProxyError::SchemaValidation { message, .. } => {
turbomcp_protocol::McpError::invalid_params(message)
}
ProxyError::Timeout {
operation,
timeout_ms,
} => {
turbomcp_protocol::McpError::timeout(format!("{operation} exceeded {timeout_ms}ms"))
}
ProxyError::RateLimitExceeded { message, .. } => {
turbomcp_protocol::McpError::rate_limited(message)
}
#[cfg(feature = "auth")]
ProxyError::Auth(message) => {
turbomcp_protocol::McpError::internal(format!("Authentication error: {message}"))
}
ProxyError::Serialization(err) => {
turbomcp_protocol::McpError::serialization(err.to_string())
}
ProxyError::Io(err) => turbomcp_protocol::McpError::transport(err.to_string()),
#[cfg(feature = "runtime")]
ProxyError::Http {
message,
status_code,
} => {
let msg = if let Some(code) = status_code {
format!("{message} (HTTP {code})")
} else {
message
};
turbomcp_protocol::McpError::transport(msg)
}
}
}
}
impl From<ProxyError> for Box<turbomcp_protocol::McpError> {
fn from(err: ProxyError) -> Self {
Box::new(turbomcp_protocol::McpError::from(err))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let err = ProxyError::introspection("test");
assert!(matches!(err, ProxyError::Introspection { .. }));
let err = ProxyError::configuration("test");
assert!(matches!(err, ProxyError::Configuration { .. }));
}
#[test]
fn test_error_creation_with_context() {
let err = ProxyError::introspection_with_context("failed", "stdio backend");
match err {
ProxyError::Introspection { message, context } => {
assert_eq!(message, "failed");
assert_eq!(context, Some("stdio backend".to_string()));
}
_ => panic!("Wrong error type"),
}
}
#[test]
fn test_error_display() {
let err = ProxyError::introspection("failed to connect");
assert!(err.to_string().contains("Introspection error"));
assert!(err.to_string().contains("failed to connect"));
}
#[test]
fn test_error_sanitization() {
let err = ProxyError::configuration_with_key("Invalid API key format", "api_key");
assert_eq!(err.sanitize(), "Configuration error");
}
#[test]
fn test_retryable_errors() {
let err = ProxyError::timeout("tool_call", 30000);
assert!(err.is_retryable());
let err = ProxyError::configuration("bad config");
assert!(!err.is_retryable());
}
#[test]
fn test_protocol_error_preservation() {
let protocol_err = turbomcp_protocol::Error::user_rejected("User cancelled");
let proxy_err = ProxyError::from(protocol_err);
let back_to_protocol: Box<turbomcp_protocol::Error> = proxy_err.into();
assert_eq!(
back_to_protocol.kind,
turbomcp_protocol::ErrorKind::UserRejected
);
}
#[test]
fn test_error_ext_trait() {
let result: Result<String, std::io::Error> = Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found",
));
let proxy_result = result.introspection_context("reading config");
assert!(proxy_result.is_err());
match proxy_result.unwrap_err() {
ProxyError::Introspection { message, context } => {
assert!(message.contains("file not found"));
assert_eq!(context, Some("reading config".to_string()));
}
_ => panic!("Wrong error type"),
}
}
}