From 5ccd58dbe95b549ab45abc9b2a6c17a23d9ff47e Mon Sep 17 00:00:00 2001 From: Ron Nakamoto <14256602+ronnakamoto@users.noreply.github.com> Date: Sat, 17 May 2025 03:58:35 +0530 Subject: [PATCH 1/3] fix: added error handling for connection issues --- cli/lib.rs | 227 ++++++++++++++++++++++++++++++++++++++++++++++++++-- cli/main.rs | 30 +++++-- 2 files changed, 243 insertions(+), 14 deletions(-) diff --git a/cli/lib.rs b/cli/lib.rs index c75bf54..d78a676 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -1,4 +1,4 @@ -use std::{net::SocketAddr, time::Duration}; +use std::{fmt, io, net::SocketAddr, time::Duration}; use clap::{Parser, Subcommand}; use http::HeaderMap; @@ -8,6 +8,128 @@ use thunder::types::{Address, Txid}; use thunder_app_rpc_api::RpcClient; use tracing_subscriber::layer::SubscriberExt as _; +/// Custom error type for CLI-specific errors +#[derive(Debug)] +pub enum CliError { + /// Connection error with details + ConnectionError { + /// The URL that was being connected to + url: url::Url, + /// The underlying error + source: anyhow::Error, + }, + /// Other errors + Other(anyhow::Error), +} + +impl fmt::Display for CliError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ConnectionError { url, source } => { + write!(f, "Failed to connect to Thunder node at {}", url)?; + + // Check for common connection errors and provide helpful messages + if let Some(io_err) = source.downcast_ref::() { + match io_err.kind() { + io::ErrorKind::ConnectionRefused => { + write!(f, "\nConnection refused. Please check that:")?; + write!(f, "\n 1. The Thunder node is running")?; + write!(f, "\n 2. The RPC server is enabled")?; + write!(f, "\n 3. The RPC address is correct ({})", url)?; + write!(f, "\n 4. There are no firewall rules blocking the connection")?; + } + io::ErrorKind::ConnectionReset => { + write!(f, "\nConnection reset by peer. The server might be overloaded or restarting.")?; + } + io::ErrorKind::ConnectionAborted => { + write!(f, "\nConnection aborted. The server might have terminated the connection.")?; + } + io::ErrorKind::TimedOut => { + write!(f, "\nConnection timed out. The server might be unresponsive or behind a firewall.")?; + } + io::ErrorKind::AddrNotAvailable => { + write!(f, "\nAddress not available. The specified address cannot be assigned.")?; + } + io::ErrorKind::NotConnected => { + write!(f, "\nNot connected. No connection could be established.")?; + } + io::ErrorKind::BrokenPipe => { + write!(f, "\nBroken pipe. The connection was unexpectedly closed.")?; + } + _ => { + write!(f, "\nIO Error: {} (kind: {:?})", source, io_err.kind())?; + } + } + } else { + // Check for common error patterns in the error message + let err_str = source.to_string().to_lowercase(); + + if err_str.contains("connection refused") || + err_str.contains("tcp connect error") { + write!(f, "\nConnection refused. Please check that:")?; + write!(f, "\n 1. The Thunder node is running")?; + write!(f, "\n 2. The RPC server is enabled")?; + write!(f, "\n 3. The RPC address is correct ({})", url)?; + write!(f, "\n 4. There are no firewall rules blocking the connection")?; + } else if err_str.contains("dns error") || + err_str.contains("lookup") || + err_str.contains("nodename nor servname provided") || + err_str.contains("not known") { + write!(f, "\nDNS resolution failed. Could not resolve the host name.")?; + write!(f, "\nPlease check that the host part of the URL is correct.")?; + } else if err_str.contains("timeout") || + err_str.contains("timed out") { + write!(f, "\nConnection timed out. The server might be unresponsive or behind a firewall.")?; + } else { + // For other errors, provide the details but in a more user-friendly format + let mut source_err = source.source(); + if let Some(err) = source_err { + write!(f, "\nError: {}", err)?; + + // Check if there's a more specific cause + source_err = err.source(); + if let Some(err) = source_err { + write!(f, "\nCause: {}", err)?; + } + } else { + write!(f, "\nError: {}", source)?; + } + } + + // Add a helpful suggestion for all connection errors + write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; + } + } + Self::Other(err) => write!(f, "{}", err)?, + } + Ok(()) + } +} + +impl std::error::Error for CliError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::ConnectionError { source, .. } => Some(source.as_ref()), + Self::Other(err) => err.source(), + } + } +} + +impl From for CliError { + fn from(err: anyhow::Error) -> Self { + // Try to determine if this is a connection error + if let Some(client_err) = err.downcast_ref::() { + if client_err.to_string().contains("Connect") { + // This is likely a connection error, but we don't have the URL here + // We'll handle this in the run method where we have the URL + return Self::Other(err); + } + } + + Self::Other(err) + } +} + #[derive(Clone, Debug, Subcommand)] #[command(arg_required_else_help(true))] pub enum Command { @@ -95,7 +217,8 @@ pub enum Command { #[command(author, version, about, long_about = None)] pub struct Cli { /// Base URL used for requests to the RPC server. - #[arg(default_value = "http://localhost:6009", long)] + /// If protocol is not specified, http:// will be used. + #[arg(default_value = "http://localhost:6009", long, value_parser = parse_url)] pub rpc_url: url::Url, #[arg(long, help = "Timeout for RPC requests in seconds (default: 60)")] @@ -107,6 +230,31 @@ pub struct Cli { #[command(subcommand)] pub command: Command, } + +/// Custom URL parser that adds http:// prefix if missing +fn parse_url(s: &str) -> Result { + // Check if the URL already has a scheme + if s.contains("://") { + // Try to parse the URL as-is + match url::Url::parse(s) { + Ok(url) => Ok(url), + Err(e) => Err(format!( + "Invalid URL '{}': {}. Make sure the URL format is correct (e.g., http://hostname:port).", + s, e + )), + } + } else { + // Add http:// prefix and try to parse + let url_with_scheme = format!("http://{}", s); + match url::Url::parse(&url_with_scheme) { + Ok(url) => Ok(url), + Err(e) => Err(format!( + "Invalid URL '{}': {}. Make sure the URL format is correct (e.g., http://hostname:port).", + s, e + )), + } + } +} /// Handle a command, returning CLI output async fn handle_command( rpc_client: &RpcClient, @@ -253,14 +401,14 @@ fn set_tracing_subscriber() -> anyhow::Result<()> { } impl Cli { - pub async fn run(self) -> anyhow::Result { + pub async fn run(self) -> Result { if self.verbose { - set_tracing_subscriber()?; + set_tracing_subscriber().map_err(|e| CliError::Other(e))?; } const DEFAULT_TIMEOUT: u64 = 60; - let request_id = uuid::Uuid::new_v4().as_simple().to_string(); + let request_id = uuid::Uuid::new_v4().to_string().replace("-", ""); tracing::info!("request ID: {}", request_id); @@ -271,11 +419,74 @@ impl Cli { .set_max_logging_length(1024) .set_headers(HeaderMap::from_iter([( http::header::HeaderName::from_static("x-request-id"), - http::header::HeaderValue::from_str(&request_id)?, + http::header::HeaderValue::from_str(&request_id) + .map_err(|e| CliError::Other(e.into()))?, )])); - let client = builder.build(self.rpc_url)?; - let result = handle_command(&client, self.command).await?; + // Store the URL for potential error messages + let rpc_url = self.rpc_url.clone(); + + // Build the client and handle connection errors + let client = builder.build(self.rpc_url.clone()).map_err(|err| { + // Check if this is a connection error + let err_str = err.to_string().to_lowercase(); + if err_str.contains("connect") || + err_str.contains("connection") || + err_str.contains("network") || + err_str.contains("tcp") || + err_str.contains("dns") || + err_str.contains("lookup") || + err_str.contains("timeout") || + err_str.contains("timed out") { + return CliError::ConnectionError { + url: rpc_url.clone(), + source: err.into(), + }; + } + + CliError::Other(err.into()) + })?; + + // Execute the command and handle potential connection errors + let result = handle_command(&client, self.command) + .await + .map_err(|err| { + // Check if this is a connection error that happened during the request + if let Some(client_err) = err.downcast_ref::() { + if client_err.to_string().contains("Connect") || + client_err.to_string().contains("connection") || + client_err.to_string().contains("Connection") || + client_err.to_string().contains("network") || + client_err.to_string().contains("Network") || + client_err.to_string().contains("timeout") || + client_err.to_string().contains("timed out") { + return CliError::ConnectionError { + url: rpc_url.clone(), + source: err, + }; + } + } + + // Check for transport errors + let err_str = err.to_string().to_lowercase(); + if err_str.contains("tcp connect error") || + err_str.contains("connection refused") || + err_str.contains("connection reset") || + err_str.contains("connection aborted") || + err_str.contains("broken pipe") || + err_str.contains("dns error") || + err_str.contains("lookup") || + err_str.contains("timeout") || + err_str.contains("timed out") { + return CliError::ConnectionError { + url: rpc_url.clone(), + source: err, + }; + } + + CliError::Other(err) + })?; + Ok(result) } } diff --git a/cli/main.rs b/cli/main.rs index a2072c5..75ef3bb 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -1,13 +1,31 @@ use clap::Parser; -use thunder_app_cli_lib::Cli; +use thunder_app_cli_lib::{Cli, CliError}; #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - let res = cli.run().await?; - #[allow(clippy::print_stdout)] - { - println!("{res}"); + + match cli.run().await { + Ok(res) => { + #[allow(clippy::print_stdout)] + { + println!("{res}"); + } + Ok(()) + } + Err(err) => { + match err { + CliError::ConnectionError { .. } => { + // For connection errors, we want to show a user-friendly message + // without the stack trace + eprintln!("{}", err); + std::process::exit(1); + } + CliError::Other(err) => { + // For other errors, we'll let anyhow handle it with the stack trace + Err(err) + } + } + } } - Ok(()) } From b5ef403d2b4c8ba1721a26d6bcc4d6820c542e39 Mon Sep 17 00:00:00 2001 From: Ron Nakamoto <14256602+ronnakamoto@users.noreply.github.com> Date: Sat, 17 May 2025 11:51:57 +0530 Subject: [PATCH 2/3] fix: proper fix for connection issues --- cli/lib.rs | 102 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 9 deletions(-) diff --git a/cli/lib.rs b/cli/lib.rs index d78a676..e262958 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -28,6 +28,54 @@ impl fmt::Display for CliError { Self::ConnectionError { url, source } => { write!(f, "Failed to connect to Thunder node at {}", url)?; + // First, check if this is a DNS error by examining the full error chain + let is_dns_error = { + let err_str = source.to_string().to_lowercase(); + let is_dns = err_str.contains("dns error") || + err_str.contains("lookup") || + err_str.contains("nodename nor servname provided") || + err_str.contains("not known") || + err_str.contains("name resolution") || + err_str.contains("host not found") || + err_str.contains("no such host"); + + // Also check the error chain for DNS-related errors + if !is_dns { + let mut current_err = source.source(); + let mut found_dns_error = false; + while let Some(err) = current_err { + let err_str = err.to_string().to_lowercase(); + if err_str.contains("dns error") || + err_str.contains("lookup") || + err_str.contains("nodename nor servname provided") || + err_str.contains("not known") || + err_str.contains("name resolution") || + err_str.contains("host not found") || + err_str.contains("no such host") { + found_dns_error = true; + break; + } + current_err = err.source(); + } + found_dns_error + } else { + true + } + }; + + // Handle DNS errors first, as they're a special case + if is_dns_error { + write!(f, "\n\nDNS resolution failed. Could not resolve the host name.")?; + write!(f, "\nPlease check that:")?; + write!(f, "\n 1. The hostname part of the URL is correct")?; + write!(f, "\n 2. Your network connection is working")?; + write!(f, "\n 3. DNS resolution is functioning properly")?; + + // For DNS errors, we don't add the generic "Make sure the Thunder node is running" message + // as it doesn't make sense for DNS errors + return Ok(()); + } + // Check for common connection errors and provide helpful messages if let Some(io_err) = source.downcast_ref::() { match io_err.kind() { @@ -37,27 +85,51 @@ impl fmt::Display for CliError { write!(f, "\n 2. The RPC server is enabled")?; write!(f, "\n 3. The RPC address is correct ({})", url)?; write!(f, "\n 4. There are no firewall rules blocking the connection")?; + + // Add the generic message for connection refused errors + write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; } io::ErrorKind::ConnectionReset => { write!(f, "\nConnection reset by peer. The server might be overloaded or restarting.")?; + + // Add the generic message + write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; } io::ErrorKind::ConnectionAborted => { write!(f, "\nConnection aborted. The server might have terminated the connection.")?; + + // Add the generic message + write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; } io::ErrorKind::TimedOut => { write!(f, "\nConnection timed out. The server might be unresponsive or behind a firewall.")?; + + // Add the generic message + write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; } io::ErrorKind::AddrNotAvailable => { write!(f, "\nAddress not available. The specified address cannot be assigned.")?; + + // Add the generic message + write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; } io::ErrorKind::NotConnected => { write!(f, "\nNot connected. No connection could be established.")?; + + // Add the generic message + write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; } io::ErrorKind::BrokenPipe => { write!(f, "\nBroken pipe. The connection was unexpectedly closed.")?; + + // Add the generic message + write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; } _ => { write!(f, "\nIO Error: {} (kind: {:?})", source, io_err.kind())?; + + // Add the generic message + write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; } } } else { @@ -71,15 +143,27 @@ impl fmt::Display for CliError { write!(f, "\n 2. The RPC server is enabled")?; write!(f, "\n 3. The RPC address is correct ({})", url)?; write!(f, "\n 4. There are no firewall rules blocking the connection")?; - } else if err_str.contains("dns error") || - err_str.contains("lookup") || - err_str.contains("nodename nor servname provided") || - err_str.contains("not known") { - write!(f, "\nDNS resolution failed. Could not resolve the host name.")?; - write!(f, "\nPlease check that the host part of the URL is correct.")?; + + // Add the generic message for connection refused errors + write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; } else if err_str.contains("timeout") || err_str.contains("timed out") { write!(f, "\nConnection timed out. The server might be unresponsive or behind a firewall.")?; + + // Add the generic message for timeout errors + write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; + } else if err_str.contains("connection refused") || + err_str.contains("connect error") || + err_str.contains("connect failed") { + // This is a more generic check for connection refused errors + write!(f, "\nConnection refused. Please check that:")?; + write!(f, "\n 1. The Thunder node is running")?; + write!(f, "\n 2. The RPC server is enabled")?; + write!(f, "\n 3. The RPC address is correct ({})", url)?; + write!(f, "\n 4. There are no firewall rules blocking the connection")?; + + // Add the generic message for connection refused errors + write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; } else { // For other errors, provide the details but in a more user-friendly format let mut source_err = source.source(); @@ -94,10 +178,10 @@ impl fmt::Display for CliError { } else { write!(f, "\nError: {}", source)?; } - } - // Add a helpful suggestion for all connection errors - write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; + // Add the generic message for other errors + write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; + } } } Self::Other(err) => write!(f, "{}", err)?, From e8b7155ec95c520bbe85322aab23cd151052d989 Mon Sep 17 00:00:00 2001 From: Ron Nakamoto <14256602+ronnakamoto@users.noreply.github.com> Date: Sat, 17 May 2025 12:12:41 +0530 Subject: [PATCH 3/3] fix: improved fixes --- cli/lib.rs | 254 +++++++++++++++++++++++++++++++++++----------------- cli/main.rs | 25 +++--- 2 files changed, 187 insertions(+), 92 deletions(-) diff --git a/cli/lib.rs b/cli/lib.rs index e262958..1c8b826 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -26,8 +26,6 @@ impl fmt::Display for CliError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::ConnectionError { url, source } => { - write!(f, "Failed to connect to Thunder node at {}", url)?; - // First, check if this is a DNS error by examining the full error chain let is_dns_error = { let err_str = source.to_string().to_lowercase(); @@ -65,14 +63,41 @@ impl fmt::Display for CliError { // Handle DNS errors first, as they're a special case if is_dns_error { - write!(f, "\n\nDNS resolution failed. Could not resolve the host name.")?; - write!(f, "\nPlease check that:")?; + write!(f, "Unable to connect: DNS resolution failed")?; + write!(f, "\n\nCould not resolve the host name: {}", url.host_str().unwrap_or("unknown"))?; + write!(f, "\n\nPlease check that:")?; write!(f, "\n 1. The hostname part of the URL is correct")?; write!(f, "\n 2. Your network connection is working")?; write!(f, "\n 3. DNS resolution is functioning properly")?; - // For DNS errors, we don't add the generic "Make sure the Thunder node is running" message - // as it doesn't make sense for DNS errors + return Ok(()); + } + + // Check for HTTP status code errors + let err_str = source.to_string().to_lowercase(); + if err_str.contains("404") || err_str.contains("rejected") { + write!(f, "Unable to connect: Server responded with an error")?; + write!(f, "\n\nThe server at {} responded, but it doesn't appear to be a Thunder node.", url)?; + write!(f, "\n\nPlease check that:")?; + write!(f, "\n 1. The URL is correct")?; + write!(f, "\n 2. The server at this address is running Thunder")?; + write!(f, "\n 3. You're using the correct protocol (http:// vs https://)")?; + + return Ok(()); + } + + // Check for connection closed errors + if err_str.contains("connection closed") || + err_str.contains("broken pipe") || + err_str.contains("reset by peer") { + write!(f, "Unable to connect: Connection was closed unexpectedly")?; + write!(f, "\n\nThe server at {} closed the connection.", url)?; + write!(f, "\n\nThis usually means:")?; + write!(f, "\n 1. The server is not a Thunder node")?; + write!(f, "\n 2. You're connecting to the wrong port")?; + write!(f, "\n 3. The server requires a different protocol")?; + write!(f, "\n\nPlease verify the address and port are correct.")?; + return Ok(()); } @@ -80,56 +105,66 @@ impl fmt::Display for CliError { if let Some(io_err) = source.downcast_ref::() { match io_err.kind() { io::ErrorKind::ConnectionRefused => { - write!(f, "\nConnection refused. Please check that:")?; + write!(f, "Unable to connect: Connection refused")?; + write!(f, "\n\nCould not connect to Thunder node at {}", url)?; + write!(f, "\n\nPlease check that:")?; write!(f, "\n 1. The Thunder node is running")?; write!(f, "\n 2. The RPC server is enabled")?; write!(f, "\n 3. The RPC address is correct ({})", url)?; write!(f, "\n 4. There are no firewall rules blocking the connection")?; - - // Add the generic message for connection refused errors - write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; } io::ErrorKind::ConnectionReset => { - write!(f, "\nConnection reset by peer. The server might be overloaded or restarting.")?; - - // Add the generic message - write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; + write!(f, "Unable to connect: Connection reset")?; + write!(f, "\n\nThe connection was reset by the server at {}", url)?; + write!(f, "\n\nThis usually happens when:")?; + write!(f, "\n 1. The server is overloaded")?; + write!(f, "\n 2. The server is restarting")?; + write!(f, "\n 3. There's a network issue between you and the server")?; + write!(f, "\n\nPlease try again in a few moments.")?; } io::ErrorKind::ConnectionAborted => { - write!(f, "\nConnection aborted. The server might have terminated the connection.")?; - - // Add the generic message - write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; + write!(f, "Unable to connect: Connection aborted")?; + write!(f, "\n\nThe connection was aborted while trying to reach {}", url)?; + write!(f, "\n\nThis usually happens when:")?; + write!(f, "\n 1. The server terminated the connection")?; + write!(f, "\n 2. A network device (like a firewall) interrupted the connection")?; + write!(f, "\n\nPlease check your network settings and try again.")?; } io::ErrorKind::TimedOut => { - write!(f, "\nConnection timed out. The server might be unresponsive or behind a firewall.")?; - - // Add the generic message - write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; + write!(f, "Unable to connect: Connection timed out")?; + write!(f, "\n\nThe connection to {} timed out.", url)?; + write!(f, "\n\nThis usually means:")?; + write!(f, "\n 1. The server is not responding")?; + write!(f, "\n 2. A firewall is blocking the connection")?; + write!(f, "\n 3. The network path to the server is congested or down")?; + write!(f, "\n\nPlease check that the server is running and accessible.")?; } io::ErrorKind::AddrNotAvailable => { - write!(f, "\nAddress not available. The specified address cannot be assigned.")?; - - // Add the generic message - write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; + write!(f, "Unable to connect: Address not available")?; + write!(f, "\n\nThe address {} cannot be used.", url)?; + write!(f, "\n\nThis usually happens when:")?; + write!(f, "\n 1. The IP address is not valid on this network")?; + write!(f, "\n 2. The port is already in use or reserved")?; + write!(f, "\n\nPlease try a different address or port.")?; } io::ErrorKind::NotConnected => { - write!(f, "\nNot connected. No connection could be established.")?; - - // Add the generic message - write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; + write!(f, "Unable to connect: Not connected")?; + write!(f, "\n\nCould not establish a connection to {}", url)?; + write!(f, "\n\nPlease check your network connection and try again.")?; } io::ErrorKind::BrokenPipe => { - write!(f, "\nBroken pipe. The connection was unexpectedly closed.")?; - - // Add the generic message - write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; + write!(f, "Unable to connect: Connection broken")?; + write!(f, "\n\nThe connection to {} was unexpectedly closed.", url)?; + write!(f, "\n\nThis usually happens when the server closes the connection.")?; + write!(f, "\n\nPlease check that the server is running and try again.")?; } _ => { - write!(f, "\nIO Error: {} (kind: {:?})", source, io_err.kind())?; - - // Add the generic message - write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; + write!(f, "Unable to connect to Thunder node at {}", url)?; + write!(f, "\n\nAn unexpected network error occurred.")?; + write!(f, "\n\nPlease check that:")?; + write!(f, "\n 1. The Thunder node is running")?; + write!(f, "\n 2. The address is correct")?; + write!(f, "\n 3. Your network connection is working")?; } } } else { @@ -137,54 +172,67 @@ impl fmt::Display for CliError { let err_str = source.to_string().to_lowercase(); if err_str.contains("connection refused") || - err_str.contains("tcp connect error") { - write!(f, "\nConnection refused. Please check that:")?; + err_str.contains("tcp connect error") || + err_str.contains("connect error") || + err_str.contains("connect failed") || + err_str.contains("os error 61") || + err_str.contains("client error (connect)") { + write!(f, "Unable to connect: Connection refused")?; + write!(f, "\n\nCould not connect to Thunder node at {}", url)?; + write!(f, "\n\nPlease check that:")?; write!(f, "\n 1. The Thunder node is running")?; write!(f, "\n 2. The RPC server is enabled")?; write!(f, "\n 3. The RPC address is correct ({})", url)?; write!(f, "\n 4. There are no firewall rules blocking the connection")?; - - // Add the generic message for connection refused errors - write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; } else if err_str.contains("timeout") || err_str.contains("timed out") { - write!(f, "\nConnection timed out. The server might be unresponsive or behind a firewall.")?; - - // Add the generic message for timeout errors - write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; - } else if err_str.contains("connection refused") || - err_str.contains("connect error") || - err_str.contains("connect failed") { - // This is a more generic check for connection refused errors - write!(f, "\nConnection refused. Please check that:")?; + write!(f, "Unable to connect: Connection timed out")?; + write!(f, "\n\nThe connection to {} timed out.", url)?; + write!(f, "\n\nThis usually means:")?; + write!(f, "\n 1. The server is not responding")?; + write!(f, "\n 2. A firewall is blocking the connection")?; + write!(f, "\n 3. The network path to the server is congested or down")?; + write!(f, "\n\nPlease check that the server is running and accessible.")?; + } else { + // For other errors, provide a generic but user-friendly message + write!(f, "Unable to connect to Thunder node at {}", url)?; + write!(f, "\n\nAn unexpected error occurred while trying to connect.")?; + write!(f, "\n\nPlease check that:")?; write!(f, "\n 1. The Thunder node is running")?; - write!(f, "\n 2. The RPC server is enabled")?; - write!(f, "\n 3. The RPC address is correct ({})", url)?; - write!(f, "\n 4. There are no firewall rules blocking the connection")?; + write!(f, "\n 2. The address is correct")?; + write!(f, "\n 3. Your network connection is working")?; - // Add the generic message for connection refused errors - write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; - } else { - // For other errors, provide the details but in a more user-friendly format - let mut source_err = source.source(); - if let Some(err) = source_err { - write!(f, "\nError: {}", err)?; - - // Check if there's a more specific cause - source_err = err.source(); - if let Some(err) = source_err { - write!(f, "\nCause: {}", err)?; - } - } else { - write!(f, "\nError: {}", source)?; + // If in verbose mode or for debugging, we can include the technical details + if std::env::var("THUNDER_VERBOSE").is_ok() { + write!(f, "\n\nTechnical details (for support):")?; + write!(f, "\n{}", source)?; } + } + } + } + Self::Other(err) => { + // For other errors, we'll try to make them more user-friendly too + let err_str = err.to_string().to_lowercase(); - // Add the generic message for other errors - write!(f, "\n\nMake sure the Thunder node is running and accessible at {}", url)?; + if err_str.contains("404") || err_str.contains("rejected") { + write!(f, "Unable to connect: Server responded with an error")?; + write!(f, "\n\nThe server responded, but it doesn't appear to be a Thunder node.")?; + write!(f, "\n\nPlease check that:")?; + write!(f, "\n 1. The URL is correct")?; + write!(f, "\n 2. The server at this address is running Thunder")?; + write!(f, "\n 3. You're using the correct protocol (http:// vs https://)")?; + } else { + // For truly unknown errors, just pass through but with a nicer format + write!(f, "Error: {}", err)?; + + // If in verbose mode or for debugging, we can include more technical details + if std::env::var("THUNDER_VERBOSE").is_ok() { + if let Some(source) = err.source() { + write!(f, "\n\nCaused by: {}", source)?; + } } } } - Self::Other(err) => write!(f, "{}", err)?, } Ok(()) } @@ -322,20 +370,62 @@ fn parse_url(s: &str) -> Result { // Try to parse the URL as-is match url::Url::parse(s) { Ok(url) => Ok(url), - Err(e) => Err(format!( - "Invalid URL '{}': {}. Make sure the URL format is correct (e.g., http://hostname:port).", - s, e - )), + Err(e) => { + let error_msg = e.to_string().to_lowercase(); + + if error_msg.contains("invalid ip") || error_msg.contains("invalid ipv") { + Err(format!( + "Invalid IP address in URL: '{}'.\n\nPlease provide a valid IP address format.", + s + )) + } else if error_msg.contains("invalid port") { + Err(format!( + "Invalid port in URL: '{}'.\n\nPorts must be numbers between 1-65535.", + s + )) + } else if error_msg.contains("relative url") || error_msg.contains("empty host") { + Err(format!( + "Invalid URL format: '{}'.\n\nPlease provide a complete URL in the format: http://hostname:port", + s + )) + } else { + Err(format!( + "Invalid URL: '{}'.\n\nPlease provide a valid URL in the format: http://hostname:port", + s + )) + } + } } } else { // Add http:// prefix and try to parse let url_with_scheme = format!("http://{}", s); match url::Url::parse(&url_with_scheme) { Ok(url) => Ok(url), - Err(e) => Err(format!( - "Invalid URL '{}': {}. Make sure the URL format is correct (e.g., http://hostname:port).", - s, e - )), + Err(e) => { + let error_msg = e.to_string().to_lowercase(); + + if error_msg.contains("invalid ip") || error_msg.contains("invalid ipv") { + Err(format!( + "Invalid IP address: '{}'.\n\nPlease provide a valid IP address format.", + s + )) + } else if error_msg.contains("invalid port") { + Err(format!( + "Invalid port: '{}'.\n\nPorts must be numbers between 1-65535.", + s + )) + } else if error_msg.contains("relative url") || error_msg.contains("empty host") { + Err(format!( + "Invalid URL format: '{}'.\n\nPlease provide a complete URL in the format: hostname:port", + s + )) + } else { + Err(format!( + "Invalid URL: '{}'.\n\nPlease provide a valid URL in the format: hostname:port", + s + )) + } + } } } } @@ -492,7 +582,7 @@ impl Cli { const DEFAULT_TIMEOUT: u64 = 60; - let request_id = uuid::Uuid::new_v4().to_string().replace("-", ""); + let request_id = uuid::Uuid::new_v4().simple().to_string(); tracing::info!("request ID: {}", request_id); diff --git a/cli/main.rs b/cli/main.rs index 75ef3bb..fcc63c8 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -14,17 +14,22 @@ async fn main() -> anyhow::Result<()> { Ok(()) } Err(err) => { - match err { - CliError::ConnectionError { .. } => { - // For connection errors, we want to show a user-friendly message - // without the stack trace - eprintln!("{}", err); - std::process::exit(1); - } - CliError::Other(err) => { - // For other errors, we'll let anyhow handle it with the stack trace - Err(err) + // For all errors, we want to show a user-friendly message without the stack trace + // unless THUNDER_DEBUG is set + if std::env::var("THUNDER_DEBUG").is_ok() { + match err { + CliError::ConnectionError { .. } => { + eprintln!("{}", err); + std::process::exit(1); + } + CliError::Other(err) => { + // For other errors, we'll let anyhow handle it with the stack trace + Err(err) + } } + } else { + eprintln!("{}", err); + std::process::exit(1); } } }