#![doc = include_str!("../README.md")] use std::{borrow::Cow, fmt, io, path::PathBuf, result, time}; use thiserror::Error; pub type Result = result::Result; /// Convert `Option` → `Result` without panicking. /// /// Replaces `.unwrap()` in query paths so a missing value returns /// HTTP 500 instead of crashing the server (`panic = "abort"`). pub trait OptionData { fn data(self) -> Result; } impl OptionData for Option { #[inline] fn data(self) -> Result { self.ok_or(Error::Internal("data unavailable")) } } #[derive(Debug, Error)] pub enum Error { #[error(transparent)] IO(#[from] io::Error), #[cfg(feature = "corepc")] #[error(transparent)] CorepcRPC(#[from] corepc_jsonrpc::error::Error), #[cfg(feature = "jiff")] #[error(transparent)] Jiff(#[from] jiff::Error), #[cfg(feature = "fjall")] #[error(transparent)] Fjall(#[from] fjall::Error), #[cfg(feature = "vecdb")] #[error(transparent)] VecDB(#[from] vecdb::Error), #[cfg(feature = "vecdb")] #[error(transparent)] RawDB(#[from] vecdb::RawDBError), #[cfg(feature = "ureq")] #[error(transparent)] Ureq(#[from] ureq::Error), #[error(transparent)] SystemTimeError(#[from] time::SystemTimeError), #[cfg(feature = "bitcoin")] #[error(transparent)] BitcoinConsensusEncode(#[from] bitcoin::consensus::encode::Error), #[cfg(feature = "bitcoin")] #[error(transparent)] BitcoinBip34Error(#[from] bitcoin::block::Bip34Error), #[cfg(feature = "bitcoin")] #[error(transparent)] BitcoinHexError(#[from] bitcoin::consensus::encode::FromHexError), #[cfg(feature = "bitcoin")] #[error(transparent)] BitcoinFromScriptError(#[from] bitcoin::address::FromScriptError), #[cfg(feature = "bitcoin")] #[error(transparent)] BitcoinHexToArrayError(#[from] bitcoin::hex::HexToArrayError), #[cfg(feature = "pco")] #[error(transparent)] Pco(#[from] pco::errors::PcoError), #[cfg(feature = "serde_json")] #[error(transparent)] SerdeJSON(#[from] serde_json::Error), #[cfg(feature = "tokio")] #[error(transparent)] TokioJoin(#[from] tokio::task::JoinError), #[error("ZeroCopy error")] ZeroCopyError, #[error("Wrong length, expected: {expected}, received: {received}")] WrongLength { expected: usize, received: usize }, #[error("Wrong address type")] WrongAddrType, #[error("Date cannot be indexed, must be 2009-01-03, 2009-01-09 or greater")] UnindexableDate, #[error("Quick cache error")] QuickCacheError, #[error("The provided address appears to be invalid")] InvalidAddr, #[error("Invalid network")] InvalidNetwork, #[error("The provided TXID appears to be invalid")] InvalidTxid, #[error("Mempool data is not available")] MempoolNotAvailable, #[error("Address not found in the blockchain (no transaction history)")] UnknownAddr, #[error("Failed to find the TXID in the blockchain")] UnknownTxid, #[error("Unsupported type ({0})")] UnsupportedType(String), // Generic errors with context #[error("{0}")] NotFound(String), #[error("{0}")] OutOfRange(Cow<'static, str>), #[error("{0}")] Parse(String), #[error("Internal error: {0}")] Internal(&'static str), #[error("Authentication failed")] AuthFailed, // Series-specific errors #[error("{0}")] SeriesNotFound(SeriesNotFound), #[error("'{series}' doesn't support the requested index. Try: {supported}")] SeriesUnsupportedIndex { series: String, supported: String }, #[error("No series specified")] NoSeries, #[error("No data available")] NoData, #[error("Request weight {requested} exceeds maximum {max}")] WeightExceeded { requested: usize, max: usize }, #[error("Too many unspent transaction outputs (>1000).")] TooManyUtxos, #[error("Deserialization error: {0}")] Deserialization(String), #[error("Fetch failed after retries: {0}")] FetchFailed(String), #[error("HTTP {status}: {url}")] HttpStatus { status: u16, url: String }, #[error("Version mismatch at {path:?}: expected {expected}, found {found}")] VersionMismatch { path: PathBuf, expected: usize, found: usize, }, } impl Error { /// Returns true if this error is due to a file lock (another process has the database open). /// Lock errors are transient and should not trigger data deletion. #[cfg(feature = "vecdb")] pub fn is_lock_error(&self) -> bool { matches!(self, Error::VecDB(e) if e.is_lock_error()) } /// Returns true if this error indicates data corruption or version incompatibility. /// These errors may require resetting/deleting the data to recover. #[cfg(feature = "vecdb")] pub fn is_data_error(&self) -> bool { matches!(self, Error::VecDB(e) if e.is_data_error()) || matches!(self, Error::VersionMismatch { .. }) } /// Returns true if this network/fetch error indicates a permanent/blocking condition /// that won't be resolved by retrying (e.g., DNS failure, connection refused, blocked endpoint). /// Returns false for transient errors worth retrying (timeouts, rate limits, server errors). pub fn is_network_permanently_blocked(&self) -> bool { match self { #[cfg(feature = "ureq")] Error::Ureq(e) => is_ureq_error_permanent(e), Error::IO(e) => is_io_error_permanent(e), // 403 Forbidden suggests IP/geo blocking; 429 and 5xx are transient Error::HttpStatus { status, .. } => *status == 403, // Other errors are data/parsing related, not network - treat as transient _ => false, } } } #[cfg(feature = "ureq")] fn is_ureq_error_permanent(e: &ureq::Error) -> bool { let msg = format!("{:?}", e); msg.contains("nodename nor servname") || msg.contains("Name or service not known") || msg.contains("No such host") || msg.contains("connection refused") || msg.contains("Connection refused") || msg.contains("certificate") || msg.contains("SSL") || msg.contains("TLS") || msg.contains("handshake") } fn is_io_error_permanent(e: &std::io::Error) -> bool { use std::io::ErrorKind::*; match e.kind() { // Permanent errors ConnectionRefused | PermissionDenied | AddrNotAvailable => true, // Check the error message for DNS failures _ => { let msg = e.to_string(); msg.contains("nodename nor servname") || msg.contains("Name or service not known") || msg.contains("No such host") } } } /// Maximum length of a user-supplied series name in error messages before /// truncating with an ellipsis. const SERIES_NAME_MAX_DISPLAY_LEN: usize = 100; /// Truncate a user-supplied series name for inclusion in an error message, /// appending an ellipsis if it exceeds the display cap. Used for both /// `SeriesNotFound` and `SeriesUnsupportedIndex` so far-too-long names don't /// blow up the response body. pub fn truncate_series_name(mut series: String) -> String { if series.len() > SERIES_NAME_MAX_DISPLAY_LEN { series.truncate(SERIES_NAME_MAX_DISPLAY_LEN); series.push_str("..."); } series } #[derive(Debug)] pub struct SeriesNotFound { pub series: String, pub suggestions: Vec<&'static str>, pub total_matches: usize, } impl SeriesNotFound { pub fn new(series: String, suggestions: Vec<&'static str>, total_matches: usize) -> Self { Self { series: truncate_series_name(series), suggestions, total_matches, } } } impl fmt::Display for SeriesNotFound { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "'{}' not found", self.series)?; if self.suggestions.is_empty() { return Ok(()); } let quoted: Vec<_> = self.suggestions.iter().map(|s| format!("'{s}'")).collect(); write!(f, ", did you mean {}?", quoted.join(", "))?; let remaining = self.total_matches.saturating_sub(self.suggestions.len()); if remaining > 0 { write!( f, " ({remaining} more — /api/series/search?q={} for all)", self.series )?; } Ok(()) } }