Skip to main content

brk_error/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{fmt, io, path::PathBuf, result, time};
4
5use thiserror::Error;
6
7pub type Result<T, E = Error> = result::Result<T, E>;
8
9#[derive(Debug, Error)]
10pub enum Error {
11    #[error(transparent)]
12    IO(#[from] io::Error),
13
14    #[cfg(feature = "bitcoincore-rpc")]
15    #[error(transparent)]
16    BitcoinRPC(#[from] bitcoincore_rpc::Error),
17
18    #[cfg(feature = "corepc")]
19    #[error(transparent)]
20    CorepcRPC(#[from] corepc_client::client_sync::Error),
21
22    #[cfg(feature = "jiff")]
23    #[error(transparent)]
24    Jiff(#[from] jiff::Error),
25
26    #[cfg(feature = "fjall")]
27    #[error(transparent)]
28    Fjall(#[from] fjall::Error),
29
30    #[cfg(feature = "vecdb")]
31    #[error(transparent)]
32    VecDB(#[from] vecdb::Error),
33
34    #[cfg(feature = "vecdb")]
35    #[error(transparent)]
36    RawDB(#[from] vecdb::RawDBError),
37
38    #[cfg(feature = "ureq")]
39    #[error(transparent)]
40    Ureq(#[from] ureq::Error),
41
42    #[error(transparent)]
43    SystemTimeError(#[from] time::SystemTimeError),
44
45    #[cfg(feature = "bitcoin")]
46    #[error(transparent)]
47    BitcoinConsensusEncode(#[from] bitcoin::consensus::encode::Error),
48
49    #[cfg(feature = "bitcoin")]
50    #[error(transparent)]
51    BitcoinBip34Error(#[from] bitcoin::block::Bip34Error),
52
53    #[cfg(feature = "bitcoin")]
54    #[error(transparent)]
55    BitcoinHexError(#[from] bitcoin::consensus::encode::FromHexError),
56
57    #[cfg(feature = "bitcoin")]
58    #[error(transparent)]
59    BitcoinFromScriptError(#[from] bitcoin::address::FromScriptError),
60
61    #[cfg(feature = "bitcoin")]
62    #[error(transparent)]
63    BitcoinHexToArrayError(#[from] bitcoin::hex::HexToArrayError),
64
65    #[cfg(feature = "pco")]
66    #[error(transparent)]
67    Pco(#[from] pco::errors::PcoError),
68
69    #[cfg(feature = "serde_json")]
70    #[error(transparent)]
71    SerdeJSON(#[from] serde_json::Error),
72
73    #[cfg(feature = "tokio")]
74    #[error(transparent)]
75    TokioJoin(#[from] tokio::task::JoinError),
76
77    #[error("ZeroCopy error")]
78    ZeroCopyError,
79
80    #[error("Wrong length, expected: {expected}, received: {received}")]
81    WrongLength { expected: usize, received: usize },
82
83    #[error("Wrong address type")]
84    WrongAddrType,
85
86    #[error("Date cannot be indexed, must be 2009-01-03, 2009-01-09 or greater")]
87    UnindexableDate,
88
89    #[error("Quick cache error")]
90    QuickCacheError,
91
92    #[error("The provided address appears to be invalid")]
93    InvalidAddr,
94
95    #[error("Invalid network")]
96    InvalidNetwork,
97
98    #[error("The provided TXID appears to be invalid")]
99    InvalidTxid,
100
101    #[error("Mempool data is not available")]
102    MempoolNotAvailable,
103
104    #[error("Address not found in the blockchain (no transaction history)")]
105    UnknownAddr,
106
107    #[error("Failed to find the TXID in the blockchain")]
108    UnknownTxid,
109
110    #[error("Unsupported type ({0})")]
111    UnsupportedType(String),
112
113    // Generic errors with context
114    #[error("{0}")]
115    NotFound(String),
116
117    #[error("{0}")]
118    OutOfRange(String),
119
120    #[error("{0}")]
121    Parse(String),
122
123    #[error("Internal error: {0}")]
124    Internal(&'static str),
125
126    #[error("Authentication failed")]
127    AuthFailed,
128
129    // Series-specific errors
130    #[error("{0}")]
131    SeriesNotFound(SeriesNotFound),
132
133    #[error("'{series}' doesn't support the requested index. Try: {supported}")]
134    SeriesUnsupportedIndex { series: String, supported: String },
135
136    #[error("No series specified")]
137    NoSeries,
138
139    #[error("No data available")]
140    NoData,
141
142    #[error("Request weight {requested} exceeds maximum {max}")]
143    WeightExceeded { requested: usize, max: usize },
144
145    #[error("Deserialization error: {0}")]
146    Deserialization(String),
147
148    #[error("Fetch failed after retries: {0}")]
149    FetchFailed(String),
150
151    #[error("HTTP {status}: {url}")]
152    HttpStatus { status: u16, url: String },
153
154    #[error("Version mismatch at {path:?}: expected {expected}, found {found}")]
155    VersionMismatch {
156        path: PathBuf,
157        expected: usize,
158        found: usize,
159    },
160}
161
162impl Error {
163    /// Returns true if this error is due to a file lock (another process has the database open).
164    /// Lock errors are transient and should not trigger data deletion.
165    #[cfg(feature = "vecdb")]
166    pub fn is_lock_error(&self) -> bool {
167        matches!(self, Error::VecDB(e) if e.is_lock_error())
168    }
169
170    /// Returns true if this error indicates data corruption or version incompatibility.
171    /// These errors may require resetting/deleting the data to recover.
172    #[cfg(feature = "vecdb")]
173    pub fn is_data_error(&self) -> bool {
174        matches!(self, Error::VecDB(e) if e.is_data_error())
175            || matches!(self, Error::VersionMismatch { .. })
176    }
177
178    /// Returns true if this network/fetch error indicates a permanent/blocking condition
179    /// that won't be resolved by retrying (e.g., DNS failure, connection refused, blocked endpoint).
180    /// Returns false for transient errors worth retrying (timeouts, rate limits, server errors).
181    pub fn is_network_permanently_blocked(&self) -> bool {
182        match self {
183            #[cfg(feature = "ureq")]
184            Error::Ureq(e) => is_ureq_error_permanent(e),
185            Error::IO(e) => is_io_error_permanent(e),
186            // 403 Forbidden suggests IP/geo blocking; 429 and 5xx are transient
187            Error::HttpStatus { status, .. } => *status == 403,
188            // Other errors are data/parsing related, not network - treat as transient
189            _ => false,
190        }
191    }
192}
193
194#[cfg(feature = "ureq")]
195fn is_ureq_error_permanent(e: &ureq::Error) -> bool {
196    let msg = format!("{:?}", e);
197    msg.contains("nodename nor servname")
198        || msg.contains("Name or service not known")
199        || msg.contains("No such host")
200        || msg.contains("connection refused")
201        || msg.contains("Connection refused")
202        || msg.contains("certificate")
203        || msg.contains("SSL")
204        || msg.contains("TLS")
205        || msg.contains("handshake")
206}
207
208fn is_io_error_permanent(e: &std::io::Error) -> bool {
209    use std::io::ErrorKind::*;
210    match e.kind() {
211        // Permanent errors
212        ConnectionRefused | PermissionDenied | AddrNotAvailable => true,
213        // Check the error message for DNS failures
214        _ => {
215            let msg = e.to_string();
216            msg.contains("nodename nor servname")
217                || msg.contains("Name or service not known")
218                || msg.contains("No such host")
219        }
220    }
221}
222
223#[derive(Debug)]
224pub struct SeriesNotFound {
225    pub series: String,
226    pub suggestions: Vec<String>,
227    pub total_matches: usize,
228}
229
230impl SeriesNotFound {
231    pub fn new(mut series: String, all_matches: Vec<String>) -> Self {
232        let total_matches = all_matches.len();
233        let suggestions = all_matches.into_iter().take(3).collect();
234        if series.len() > 100 {
235            series.truncate(100);
236            series.push_str("...");
237        }
238        Self {
239            series,
240            suggestions,
241            total_matches,
242        }
243    }
244}
245
246impl fmt::Display for SeriesNotFound {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        write!(f, "'{}' not found", self.series)?;
249
250        if self.suggestions.is_empty() {
251            return Ok(());
252        }
253
254        let quoted: Vec<_> = self.suggestions.iter().map(|s| format!("'{s}'")).collect();
255        write!(f, ", did you mean {}?", quoted.join(", "))?;
256
257        let remaining = self.total_matches.saturating_sub(self.suggestions.len());
258        if remaining > 0 {
259            write!(
260                f,
261                " ({remaining} more — /api/series/search?q={} for all)",
262                self.series
263            )?;
264        }
265
266        Ok(())
267    }
268}