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