brk_error 0.2.5

Errors used throughout BRK
Documentation
#![doc = include_str!("../README.md")]

use std::{fmt, io, path::PathBuf, result, time};

use thiserror::Error;

pub type Result<T, E = Error> = result::Result<T, E>;

#[derive(Debug, Error)]
pub enum Error {
    #[error(transparent)]
    IO(#[from] io::Error),

    #[cfg(feature = "bitcoincore-rpc")]
    #[error(transparent)]
    BitcoinRPC(#[from] bitcoincore_rpc::Error),

    #[cfg(feature = "corepc")]
    #[error(transparent)]
    CorepcRPC(#[from] corepc_client::client_sync::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(String),

    #[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("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")
        }
    }
}

#[derive(Debug)]
pub struct SeriesNotFound {
    pub series: String,
    pub suggestions: Vec<String>,
    pub total_matches: usize,
}

impl SeriesNotFound {
    pub fn new(mut series: String, all_matches: Vec<String>) -> Self {
        let total_matches = all_matches.len();
        let suggestions = all_matches.into_iter().take(3).collect();
        if series.len() > 100 {
            series.truncate(100);
            series.push_str("...");
        }
        Self {
            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(())
    }
}